Jak zrobić bota Discord: przegląd i samouczek
Opublikowany: 2022-03-11Discord to platforma do przesyłania wiadomości w czasie rzeczywistym, która reklamuje się jako „kompleksowy czat głosowy i tekstowy dla graczy”. Ze względu na zgrabny interfejs, łatwość obsługi i rozbudowane funkcje, Discord odnotował szybki rozwój i staje się coraz bardziej popularny nawet wśród osób, które nie interesują się grami wideo. Od maja 2017 r. do maja 2018 r. baza użytkowników wzrosła z 45 milionów do ponad 130 milionów, z ponad dwukrotnie większą liczbą użytkowników dziennie niż Slack.
Jedną z najbardziej atrakcyjnych funkcji Discorda z perspektywy programisty chatbota jest solidne wsparcie dla programowalnych botów, które pomagają zintegrować Discorda ze światem zewnętrznym i zapewniają użytkownikom bardziej angażujące wrażenia. Boty są wszechobecne na Discordzie i zapewniają szeroki zakres usług, w tym pomoc w moderacji, gry, muzykę, wyszukiwanie w Internecie, przetwarzanie płatności i wiele innych.
W tym samouczku dotyczącym bota Discord zaczniemy od omówienia interfejsu użytkownika Discord oraz jego interfejsów API REST i WebSocket dla botów, zanim przejdziemy do samouczka, w którym napiszemy prostego bota Discord w JavaScript. Na koniec usłyszymy od dewelopera, według pewnych wskaźników, najpopularniejszego bota Discorda i jego doświadczeń z rozwijaniem i utrzymywaniem swojej znaczącej infrastruktury i bazy kodu.
Interfejs użytkownika Discord
Zanim omówimy szczegóły techniczne, ważne jest, aby zrozumieć, w jaki sposób użytkownik wchodzi w interakcję z Discordem i jak Discord prezentuje się użytkownikom. Sposób, w jaki prezentuje się botom, jest koncepcyjnie podobny (ale oczywiście niewizualny). W rzeczywistości oficjalne aplikacje Discord są zbudowane na tych samych interfejsach API, z których korzystają boty. Technicznie możliwe jest uruchomienie bota na zwykłym koncie użytkownika z niewielką modyfikacją, ale jest to zabronione przez warunki korzystania z usługi Discord. Boty są wymagane do działania na kontach botów.
Oto przegląd wersji 1 aplikacji Discord działającej w przeglądarce Chrome.
1 Interfejs użytkownika Discord dla aplikacji komputerowej jest praktycznie taki sam, jak aplikacja internetowa, spakowana z Electron. Aplikacja na iOS jest zbudowana z React Native. Aplikacja na Androida jest natywnym kodem Android Java.
Rozbijmy to.
1. Lista serwerów
Po lewej stronie znajduje się lista serwerów, których jestem członkiem. Jeśli znasz Slack, serwer jest analogiczny do obszaru roboczego Slack i reprezentuje grupę użytkowników, którzy mogą komunikować się ze sobą w ramach jednego lub więcej kanałów na serwerze. Serwerem zarządza jego twórca i/lub dowolny personel, który wybierze i przekaże mu obowiązki. Twórca i/lub personel określają zasady, strukturę kanałów na serwerze oraz zarządzają użytkownikami.
W moim przypadku serwer Discord API znajduje się na szczycie mojej listy serwerów. To świetne miejsce, aby uzyskać pomoc i porozmawiać z innymi programistami. Poniżej znajduje się serwer, który stworzyłem o nazwie Test . Stworzymy tam później bota. Poniżej znajduje się przycisk do utworzenia nowego serwera. Każdy może stworzyć serwer za pomocą kilku kliknięć.
Zauważ, że chociaż termin używany w interfejsie użytkownika Discord to Server , termin używany w dokumentacji programisty i API to Guild . Gdy przejdziemy do mówienia o tematach technicznych, przejdziemy do mówienia o gildiach . Te dwa terminy są wymienne.
2. Lista kanałów
Po prawej stronie listy serwerów znajduje się lista kanałów serwera, który aktualnie przeglądam (w tym przypadku serwera Discord API). Kanały można podzielić na dowolną liczbę kategorii. Na serwerze Discord API kategorie obejmują INFORMACJE, OGÓLNE i LIBY, jak pokazano. Każdy kanał funkcjonuje jako pokój rozmów, w którym użytkownicy mogą dyskutować na dowolny temat, któremu dany kanał jest poświęcony. Kanał, który aktualnie oglądamy (informacje) ma jaśniejsze tło. Kanały, które mają nowe wiadomości od czasu, gdy je ostatnio oglądaliśmy, mają biały kolor tekstu.
3. Widok kanału
To jest widok kanału, w którym możemy zobaczyć, o czym rozmawiają użytkownicy na kanale, który aktualnie oglądamy. Widzimy tutaj jedną wiadomość, tylko częściowo widoczną. Jest to lista linków do obsługi serwerów dla poszczególnych bibliotek botów Discord. Administratorzy serwera skonfigurowali ten kanał tak, aby zwykli użytkownicy, tacy jak ja, nie mogli wysyłać w nim wiadomości. Administratorzy wykorzystują ten kanał jako tablicę ogłoszeń, aby publikować ważne informacje, w których można je łatwo zobaczyć i nie zostaną zagłuszone przez czat.
4. Lista użytkowników
Po prawej stronie znajduje się lista użytkowników aktualnie online na tym serwerze. Użytkownicy są zorganizowani w różne kategorie, a ich nazwy mają różne kolory. Wynika to z pełnionych przez nich ról . Rola opisuje, w jakiej kategorii (jeśli istnieje) użytkownik powinien się pojawić, jaki powinien być kolor jego nazwy i jakie uprawnienia ma na serwerze. Użytkownik może mieć więcej niż jedną rolę (i bardzo często ma) i istnieje pewna matematyka pierwszeństwa, która określa, co się stanie w takim przypadku. Każdy użytkownik ma co najmniej rolę @everyone. Inne role są tworzone i przydzielane przez personel serwera.
5. Wprowadzanie tekstu
To jest wejście tekstowe, w którym mogę pisać i wysyłać wiadomości, jeśli mam na to pozwolenie. Ponieważ nie mam uprawnień do wysyłania wiadomości na tym kanale, nie mogę tutaj pisać.
6. Użytkownik
To jest obecny użytkownik. Ustawiłem swoją nazwę użytkownika na „Ja”, aby uniknąć pomyłki i dlatego, że nie potrafię wybierać nazw. Poniżej mojej nazwy użytkownika znajduje się numer (#9484), który jest moim wyróżnikiem. Może być wielu innych użytkowników o nazwie „Ja”, ale jestem jedynym „Ja#9484”. Możliwe jest również ustawienie pseudonimu dla siebie na podstawie serwera, dzięki czemu mogę być znany pod różnymi nazwami na różnych serwerach.
Są to podstawowe części interfejsu użytkownika Discord, ale jest też o wiele więcej. Rozpoczęcie korzystania z Discord jest łatwe, nawet bez tworzenia konta, więc poświęć chwilę, aby poszperać. Możesz wejść na Discord, odwiedzając stronę główną Discord, klikając „otwórz Discord w przeglądarce”, wybierając nazwę użytkownika i ewentualnie odtwarzając rundę lub dwie odświeżające „kliknij zdjęcia autobusu”.
Discord API
Discord API składa się z dwóch oddzielnych części: API WebSocket i REST. Ogólnie rzecz biorąc, interfejs API WebSocket służy do odbierania zdarzeń z Discord w czasie rzeczywistym, podczas gdy interfejs API REST służy do wykonywania działań wewnątrz Discord.
Interfejs API WebSocket
Interfejs API WebSocket służy do odbierania zdarzeń z Discord, w tym tworzenia wiadomości, usuwania wiadomości, zdarzeń wyrzucania/banowania użytkowników, aktualizacji uprawnień użytkowników i wielu innych. Z drugiej strony komunikacja od bota do WebSocket API jest bardziej ograniczona. Bot używa interfejsu API WebSocket do żądania połączenia, identyfikowania się, bicia serca, zarządzania połączeniami głosowymi i wykonywania kilku innych podstawowych rzeczy. Możesz przeczytać więcej szczegółów w dokumentacji bramy Discord (pojedyncze połączenie z WebSocket API jest określane jako brama). Do wykonywania innych akcji służy REST API.
Zdarzenia z interfejsu API WebSocket zawierają ładunek zawierający informacje zależne od typu zdarzenia. Na przykład wszystkim zdarzeniom tworzenia wiadomości towarzyszyć będzie obiekt użytkownika reprezentujący autora wiadomości. Jednak sam obiekt użytkownika nie zawiera wszystkich informacji, które należy wiedzieć o użytkowniku. Na przykład nie ma informacji o uprawnieniach użytkownika. Jeśli potrzebujesz więcej informacji, możesz wysłać zapytanie do interfejsu API REST, ale z powodów wyjaśnionych dalej w następnej sekcji powinieneś ogólnie uzyskać dostęp do pamięci podręcznej, którą powinieneś utworzyć z ładunków otrzymanych z poprzednich zdarzeń. Istnieje wiele wydarzeń, które dostarczają ładunki istotne dla uprawnień użytkownika, w tym między innymi Tworzenie gildii , Aktualizacja roli gildii i Aktualizacja kanału .
Bot może być obecny w maksymalnie 2500 gildii na połączenie WebSocket. Aby umożliwić botowi obecność w większej liczbie gildii, bot musi zaimplementować sharding i otworzyć kilka oddzielnych połączeń WebSocket z Discordem. Jeśli twój bot działa w jednym procesie na jednym węźle, jest to tylko dodatkowa złożoność, która może wydawać się niepotrzebna. Ale jeśli twój bot jest bardzo popularny i musi mieć swój back-end rozłożony na oddzielne węzły, obsługa shardingu Discorda znacznie ułatwia to, niż byłoby inaczej.
REST API
Interfejs Discord REST API jest używany przez boty do wykonywania większości czynności, takich jak wysyłanie wiadomości, wyrzucanie/banowanie użytkowników i aktualizowanie uprawnień użytkowników (w dużej mierze analogicznie do zdarzeń otrzymywanych z interfejsu API WebSocket). Interfejs API REST może być również używany do wysyłania zapytań o informacje; jednak boty opierają się głównie na zdarzeniach z interfejsu API WebSocket i buforują informacje otrzymane ze zdarzeń WebSocket.
Powodów jest kilka. Na przykład wysyłanie zapytań do interfejsu API REST w celu uzyskania informacji o użytkowniku za każdym razem, gdy odbierane jest zdarzenie tworzenia wiadomości , nie jest skalowane ze względu na ograniczenia szybkości interfejsu API REST. W większości przypadków jest to również zbędne, ponieważ WebSocket API dostarcza niezbędne informacje i powinieneś mieć je w swojej pamięci podręcznej.
Istnieje jednak kilka wyjątków i czasami możesz potrzebować informacji, których nie ma w Twojej pamięci podręcznej. Kiedy bot początkowo łączy się z bramą WebSocket, zdarzenie Ready i jedno zdarzenie Guild Create na gildię, w której bot jest obecny na tym odłamku, są początkowo wysyłane do bota, aby mógł wypełnić swoją pamięć podręczną bieżącym stanem. Gildia Tworzenie wydarzeń dla gęsto zaludnionych gildii zawiera tylko informacje o użytkownikach online. Jeśli Twój bot musi uzyskać informacje o użytkowniku offline, odpowiednie informacje mogą nie być obecne w Twojej pamięci podręcznej. W takim przypadku sensowne jest wysłanie żądania do REST API. Lub, jeśli często potrzebujesz informacji o użytkownikach offline, możesz zamiast tego wysłać kod opcji Zażądaj członków gildii do interfejsu API WebSocket, aby poprosić o członków gildii offline.
Innym wyjątkiem jest sytuacja, gdy aplikacja nie jest w ogóle połączona z interfejsem WebSocket API. Na przykład, jeśli Twój bot ma pulpit nawigacyjny, do którego użytkownicy mogą się logować i zmieniać ustawienia bota na swoim serwerze. Pulpit nawigacyjny może działać w osobnym procesie bez żadnych połączeń z interfejsem API WebSocket i bez pamięci podręcznej danych z Discord. Może być konieczne tylko od czasu do czasu wykonanie kilku żądań REST API. W tego rodzaju scenariuszu sensowne jest poleganie na interfejsie API REST w celu uzyskania potrzebnych informacji.
Opakowania API
Chociaż zawsze dobrym pomysłem jest zrozumienie każdego poziomu stosu technologicznego, bezpośrednie korzystanie z interfejsów Discord WebSocket i REST API jest czasochłonne, podatne na błędy, generalnie niepotrzebne i w rzeczywistości niebezpieczne.
Discord zapewnia wyselekcjonowaną listę oficjalnie sprawdzonych bibliotek i ostrzega, że:
Korzystanie z niestandardowych implementacji lub niezgodnych bibliotek, które nadużywają interfejsu API lub powodują nadmierne limity szybkości, może skutkować trwałym zablokowaniem.
Biblioteki oficjalnie sprawdzone przez Discord są na ogół dojrzałe, dobrze udokumentowane i zawierają pełne pokrycie interfejsu Discord API. Większość programistów botów nigdy nie będzie miała dobrego powodu, aby opracować niestandardową implementację, z wyjątkiem ciekawości lub odwagi!
Obecnie oficjalnie sprawdzone biblioteki obejmują implementacje dla Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust i Swift. Mogą istnieć dwie lub więcej różnych bibliotek dla wybranego języka. Wybór którego użyć może być trudną decyzją. Oprócz sprawdzenia odpowiedniej dokumentacji, możesz dołączyć do nieoficjalnego serwera Discord API i dowiedzieć się, jaka społeczność stoi za każdą biblioteką.
Jak zrobić Discord Bota
Przejdźmy do interesów. Zamierzamy stworzyć bota Discord, który będzie wisiał na naszym serwerze i nasłuchiwał webhooków od Ko-fi. Ko-fi to usługa, która umożliwia łatwe przyjmowanie darowizn na konto PayPal. Konfiguracja tam webhooków jest bardzo prosta, w przeciwieństwie do PayPala, gdzie trzeba mieć konto firmowe, więc świetnie nadaje się do celów demonstracyjnych lub przetwarzania darowizn na małą skalę.
Gdy użytkownik przekaże 10 USD lub więcej, bot przypisze mu rolę Premium Member
, która zmieni kolor jego nazwy i przeniesie go na górę listy użytkowników online. W tym projekcie użyjemy Node.js i biblioteki Discord API o nazwie Eris (link do dokumentacji: https://abal.moe/Eris/). Eris nie jest jedyną biblioteką JavaScript. Możesz zamiast tego wybrać discord.js. Kod, który napiszemy, byłby bardzo podobny.
Na marginesie, Patreon, kolejny procesor darowizn, zapewnia oficjalnego bota Discord i obsługuje konfigurowanie ról Discord jako korzyści dla współtwórców. Zamierzamy zaimplementować coś podobnego, ale oczywiście bardziej podstawowego.
Kod każdego kroku samouczka jest dostępny na GitHub (https://github.com/mistval/premium_bot). Niektóre kroki przedstawione w tym poście pomijają niezmieniony kod dla zwięzłości, więc skorzystaj z podanych linków do GitHub, jeśli uważasz, że czegoś brakuje.
Tworzenie konta bota
Zanim zaczniemy pisać kod, potrzebujemy konta bota. Zanim będziemy mogli utworzyć konto bota, potrzebujemy konta użytkownika. Aby utworzyć konto użytkownika, postępuj zgodnie z instrukcjami tutaj.
Następnie, aby utworzyć konto bota:
1) Utwórz aplikację w portalu dla programistów.
2) Podaj podstawowe informacje o aplikacji (zwróć uwagę na pokazany tutaj ID KLIENTA — będziemy go potrzebować później).
3) Dodaj użytkownika bota połączonego z aplikacją.
4) Wyłącz przełącznik PUBLIC BOT i zanotuj pokazany token bota (będziemy go również potrzebować później). Jeśli kiedykolwiek ujawnisz swój token bota, na przykład publikując go na obrazie w poście na blogu Toptal, konieczne jest natychmiastowe jego zregenerowanie. Każdy, kto posiada Twój token bota, może kontrolować konto Twojego bota i powodować potencjalnie poważne i trwałe problemy dla Ciebie i Twoich użytkowników.
5) Dodaj bota do swojej gildii testowej. Aby dodać bota do gildii, zastąp jego identyfikator klienta (pokazany wcześniej) następującym identyfikatorem URI i przejdź do niego w przeglądarce.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Po kliknięciu autoryzuj bot jest teraz w mojej gildii testowej i widzę go na liście użytkowników. Jest offline, ale wkrótce to naprawimy.
Tworzenie projektu
Zakładając, że masz zainstalowany Node.js, utwórz projekt i zainstaluj Eris (bibliotekę botów, której będziemy używać), Express (framework aplikacji internetowej, którego użyjemy do stworzenia odbiornika webhooka) i body-parser (do analizowania treści webhooka ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Uzyskiwanie bota online i responsywne
Zacznijmy od małych kroków. Najpierw po prostu uruchomimy bota i odpowie nam. Możemy to zrobić w 10-20 liniach kodu. Wewnątrz nowego pliku bot.js musimy utworzyć instancję klienta Eris, przekazać jej nasz token bota (nabyty podczas tworzenia aplikacji bota powyżej), zasubskrybować niektóre zdarzenia w instancji klienta i powiedzieć mu, aby połączył się z Discord . W celach demonstracyjnych zakodujemy nasz token bota na stałe w pliku bot.js, ale dobrą praktyką jest utworzenie oddzielnego pliku konfiguracyjnego i zwolnienie go z kontroli źródła.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step1.js)
const eris = require('eris'); // Create a Client instance with our bot token. const bot = new eris.Client('my_token'); // When the bot is connected and ready, log to console. bot.on('ready', () => { console.log('Connected and ready.'); }); // Every time a message is sent anywhere the bot is present, // this event will fire and we will check if the bot was mentioned. // If it was, the bot will attempt to respond with "Present". bot.on('messageCreate', async (msg) => { const botWasMentioned = msg.mentions.find( mentionedUser => mentionedUser.id === bot.user.id, ); if (botWasMentioned) { try { await msg.channel.createMessage('Present'); } catch (err) { // There are various reasons why sending a message may fail. // The API might time out or choke and return a 5xx status, // or the bot may not have permission to send the // message (403 status). console.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Jeśli wszystko pójdzie dobrze, gdy uruchomisz ten kod z własnym tokenem bota, Connected and ready.
zostaną wydrukowane w konsoli i zobaczysz, że Twój bot wchodzi w tryb online na serwerze testowym. Możesz wspomnieć o 2 swoim bocie, klikając go prawym przyciskiem myszy i wybierając „Wzmianka” lub wpisując jego nazwę poprzedzoną znakiem @. Bot powinien odpowiedzieć, mówiąc „Present”.
2 Wzmianki to sposób na zwrócenie uwagi innego użytkownika, nawet jeśli nie jest on obecny. Zwykły użytkownik, gdy zostanie wspomniany, zostanie powiadomiony przez powiadomienie na pulpicie, powiadomienie na telefon komórkowy i/lub małą czerwoną ikonę wyświetlaną nad ikoną Discorda w zasobniku systemowym. Sposób (sposoby) powiadamiania użytkownika zależy od jego ustawień i stanu online. Z drugiej strony boty nie otrzymują żadnego specjalnego powiadomienia, gdy są wspomniane. Otrzymują regularne zdarzenie tworzenia wiadomości, tak jak w przypadku każdej innej wiadomości, i mogą sprawdzić wzmianki dołączone do zdarzenia, aby ustalić, czy zostały wspomniane.
Polecenie rekordu płatności
Teraz, gdy wiemy, że możemy uruchomić bota online, pozbądźmy się naszego obecnego programu obsługi zdarzeń Message Create i stwórzmy nowy, który pozwoli nam poinformować bota, że otrzymaliśmy płatność od użytkownika.
Aby poinformować bota o wpłacie wydamy polecenie, które wygląda tak:
pb!addpayment @user_mention payment_amount
Na przykład pb!addpayment @Me 10.00
, aby zarejestrować dokonaną przeze mnie płatność w wysokości 10,00 USD.
Pb! część jest określana jako przedrostek polecenia. Dobrą konwencją jest wybieranie prefiksu, od którego muszą zaczynać się wszystkie polecenia dla twojego bota. Tworzy to miarę przestrzeni nazw dla botów i pomaga uniknąć kolizji z innymi botami. Większość botów zawiera polecenie pomocy, ale wyobraź sobie bałagan, gdybyś miał dziesięć botów w swojej gildii i wszystkie odpowiedziały na pomoc ! Korzystanie z pb! jako prefiks nie jest niezawodnym rozwiązaniem, ponieważ mogą istnieć inne boty, które również używają tego samego prefiksu. Większość popularnych botów pozwala na konfigurowanie prefiksu dla każdej gildii, aby zapobiec kolizji. Inną opcją jest użycie własnej wzmianki bota jako przedrostka, chociaż to sprawia, że wydawanie poleceń jest bardziej szczegółowe.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step2.js)
const eris = require('eris'); const PREFIX = 'pb!'; const bot = new eris.Client('my_token'); const commandHandlerForCommandName = {}; commandHandlerForCommandName['addpayment'] = (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }; bot.on('messageCreate', async (msg) => { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts of the command and the command name const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the appropriate handler for the command, if there is one. const commandHandler = commandHandlerForCommandName[commandName]; if (!commandHandler) { return; } // Separate the command arguments from the command prefix and command name. const args = parts.slice(1); try { // Execute the command. await commandHandler(msg, args); } catch (err) { console.warn('Error handling command'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Spróbujmy.
Nie tylko bot zareagował na polecenie pb!addpayment
, ale stworzyliśmy uogólniony wzorzec obsługi poleceń. Możemy dodać więcej poleceń, dodając więcej modułów obsługi do słownika commandHandlerForCommandName
. Mamy tutaj zadatki na prosty framework poleceń. Obsługa poleceń jest tak fundamentalną częścią tworzenia bota, że wiele osób ma napisane i otwarte struktury poleceń, których można używać zamiast pisać własne. Struktury poleceń często umożliwiają określenie czasów oczekiwania, wymaganych uprawnień użytkownika, aliasów poleceń, opisów poleceń i przykładów użycia (dla automatycznie generowanego polecenia pomocy) i nie tylko. Eris ma wbudowaną strukturę poleceń.
Mówiąc o uprawnieniach, nasz bot ma mały problem z bezpieczeństwem. Każdy może wykonać polecenie addpayment
. Ograniczmy to tak, aby tylko właściciel bota mógł z niego korzystać. Dokonamy refaktoryzacji słownika commandHandlerForCommandName
i sprawimy, że będzie zawierał obiekty JavaScript jako jego wartości. Te obiekty będą zawierać właściwość execute
z procedurą obsługi poleceń oraz właściwość botOwnerOnly
z wartością logiczną. Zakodujemy również nasz identyfikator użytkownika w sekcji stałych bota, aby wiedział, kto jest jego właścicielem. Możesz znaleźć swój identyfikator użytkownika, włączając tryb programisty w ustawieniach Discord, a następnie klikając prawym przyciskiem myszy swoją nazwę użytkownika i wybierając opcję Kopiuj identyfikator.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step3.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const bot = new eris.Client('my_token'); const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Teraz bot ze złością odmówi wykonania polecenia addpayment
, jeśli ktoś inny niż właściciel bota spróbuje je wykonać.

Następnie niech bot przypisze rolę członka Premium Member
każdemu, kto przekaże dziesięć dolarów lub więcej. W górnej części pliku bot.js:
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step4.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
Teraz mogę spróbować powiedzieć pb!addpayment @Me 10.00
, a bot powinien przydzielić mi rolę Premium Member
.
Ups, w konsoli pojawia się błąd brakujących uprawnień.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
Bot nie ma uprawnień do zarządzania rolami w gildii testowej, więc nie może tworzyć ani przypisywać ról. Moglibyśmy nadać botowi uprawnienia administratora i nigdy więcej nie mielibyśmy tego typu problemów, ale tak jak w przypadku każdego systemu, najlepiej jest dać użytkownikowi (lub w tym przypadku botowi) minimalne uprawnienia, których potrzebuje
Możemy nadać botowi uprawnienia do zarządzania rolami, tworząc rolę w ustawieniach serwera, włączając uprawnienia do zarządzania rolami dla tej roli i przypisując tę rolę botowi.
Teraz, gdy próbuję ponownie wykonać polecenie, rola jest tworzona i przypisywana do mnie, a ja mam wymyślny kolor nazwy i specjalną pozycję na liście członków.
W module obsługi poleceń mamy komentarz TODO sugerujący, że musimy sprawdzić, czy nie ma nieprawidłowych argumentów. Zajmijmy się tym teraz.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
Oto pełny kod do tej pory:
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
To powinno dać ci dobry podstawowy pomysł, jak stworzyć bota Discord. Teraz zobaczymy, jak zintegrować bota z Ko-fi. Jeśli chcesz, możesz utworzyć webhooka na swoim pulpicie nawigacyjnym w Ko-fi, upewnić się, że router jest skonfigurowany do przekazywania portu 80 i wysyłać do siebie rzeczywiste webhooki testowe. Ale użyję Postmana tylko do symulacji żądań.
Webhooki firmy Ko-fi dostarczają ładunki, które wyglądają tak:
data: { "message_id":"3a1fac0c-f960-4506-a60e-824979a74e74", "timestamp":"2017-08-21T13:04:30.7296166Z", "type":"Donation","from_name":"John Smith", "message":"Good luck with the integration!", "amount":"3.00", "url":"https://ko-fi.com" }
Utwórzmy nowy plik źródłowy o nazwie webhook_listener.js i użyjmy Express do nasłuchiwania webhooków. Będziemy mieć tylko jedną trasę Express i to w celach demonstracyjnych, więc nie będziemy się zbytnio przejmować używaniem idiomatycznej struktury katalogów. Po prostu umieścimy całą logikę serwera WWW w jednym pliku.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/webhook_listener_step6.js)
const express = require('express'); const app = express(); const PORT = process.env.PORT || 80; class WebhookListener { listen() { app.get('/kofi', (req, res) => { res.send('Hello'); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
Następnie zażądajmy nowego pliku na górze bot.js, aby słuchacz uruchamiał się, gdy uruchamiamy bot.js.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Po uruchomieniu bota powinieneś zobaczyć „Hello”, gdy przejdziesz do http://localhost/kofi w swojej przeglądarce.
Teraz niech WebhookListener
przetworzy dane z elementu webhook i wyemituje zdarzenie. A teraz, gdy przetestowaliśmy, że nasza przeglądarka może uzyskać dostęp do trasy, zmieńmy trasę na trasę POST, ponieważ webhook z Ko-fi będzie żądaniem POST.
(Link do kodu GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
const express = require('express'); const bodyParser = require('body-parser'); const EventEmitter = require('events'); const PORT = process.env.PORT || 80; const app = express(); app.use(bodyParser.json()); class WebhookListener extends EventEmitter { listen() { app.post('/kofi', (req, res) => { const data = req.body.data; const { message, timestamp } = data; const amount = parseFloat(data.amount); const senderName = data.from_name; const paymentId = data.message_id; const paymentSource = 'Ko-fi'; // The OK is just for us to see in Postman. Ko-fi doesn't care // about the response body, it just wants a 200. res.send({ status: 'OK' }); this.emit( 'donation', paymentSource, paymentId, timestamp, amount, senderName, message, ); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
Next we need to have the bot listen for the event, decide which user donated, and assign them a role. To decide which user donated, we'll try to find a user whose username is a substring of the message received from Ko-fi. Donors must be instructed to provide their username (with the discriminator) in the message than they write when they make their donation.
At the bottom of bot.js:
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
function findUserInString(str) { const lowercaseStr = str.toLowerCase(); // Look for a matching username in the form of username#discriminator. const user = bot.users.find( user => lowercaseStr.indexOf(`${user.username.toLowerCase()}#${user.discriminator}`) !== -1, ); return user; } async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await updateMemberRoleForDonation(guild, guildMember, amount); } catch (err) { console.warn('Error handling donation event.'); console.warn(err); } } webhookListener.on('donation', onDonation); bot.connect();
In the onDonation
function, we see two representations of a user: as a User, and as a Member. These both represent the same person, but the Member object contains guild-specific information about the User, such as their roles in the guild and their nickname. Since we want to add a role, we need to use the Member representation of the user. Each User in Discord has one Member representation for each guild that they are in.
Now I can use Postman to test the code.
I receive a 200 status code, and I get the role granted to me in the server.
If the message from Ko-fi does not contain a valid username; however, nothing happens. The donor doesn't get a role, and we are not aware that we received an orphaned donation. Let's add a log for logging donations, including donations that can't be attributed to a guild member.
First we need to create a log channel in Discord and get its channel ID. The channel ID can be found using the developer tools, which can be enabled in Discord's settings. Then you can right-click any channel and click “Copy ID.”
The log channel ID should be added to the constants section of bot.js.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
const LOG_CHANNEL_;
And then we can write a logDonation
function.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
function logDonation(member, donationAmount, paymentSource, paymentId, senderName, message, timestamp) { const isKnownMember = !!member; const memberName = isKnownMember ? `${member.username}#${member.discriminator}` : 'Unknown'; const embedColor = isKnownMember ? 0x00ff00 : 0xff0000; const logMessage = { embed: { title: 'Donation received', color: embedColor, timestamp: timestamp, fields: [ { name: 'Payment Source', value: paymentSource, inline: true }, { name: 'Payment ID', value: paymentId, inline: true }, { name: 'Sender', value: senderName, inline: true }, { name: 'Donor Discord name', value: memberName, inline: true }, { name: 'Donation amount', value: donationAmount.toString(), inline: true }, { name: 'Message', value: message, inline: true }, ], } } bot.createMessage(LOG_CHANNEL_ID, logMessage); }
Now we can update onDonation
to call the log function:
async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await Promise.all([ updateMemberRoleForDonation(guild, guildMember, amount), logDonation(guildMember, amount, paymentSource, paymentId, senderName, message, timestamp), ]); } catch (err) { console.warn('Error updating donor role and logging donation'); console.warn(err); } }
Now I can invoke the webhook again, first with a valid username, and then without one, and I get two nice log messages in the log channel.
Previously, we were just sending strings to Discord to display as messages. The more complex JavaScript object that we create and send to Discord in the new logDonation
function is a special type of message referred to as a rich embed. An embed gives you some scaffolding for making attractive messages like those shown. Only bots can create embeds, users cannot.
Now we are being notified of donations, logging them, and rewarding our supporters. We can also add donations manually with the addpayment command in case a user forgets to specify their username when they donate. Let's call it a day.
The completed code for this tutorial is available on GitHub here https://github.com/mistval/premium_bot
Następne kroki
We've successfully created a bot that can help us track donations. Is this something we can actually use? Well, perhaps. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:
- If a user leaves our guild (or if they weren't even in our guild in the first place), they will lose their
Premium Member
role, and if they rejoin, they won't get it back. We should store payments by user ID in a database, so if a premium member rejoins, we can give them their role back and maybe send them a nice welcome-back message if we were so inclined. - Paying in installments won't work. If a user sends $5 and then later sends another $5, they won't get a premium role. Similar to the above issue, storing payments in a database and issuing the
Premium Member
role when the total payments from a user reaches $10 would help here. - It's possible to receive the same webhook more than once, and this bot will record the payment multiple times. If Ko-fi doesn't receive or doesn't properly acknowledge a code 200 response from the webhook listener, it will try to send the webhook again later. Keeping track of payments in a database and ignoring webhooks with the same ID as previously received ones would help here.
- Our webhook listener isn't very secure. Anyone could forge a webhook and get a
Premium Member
role for free. Ko-fi doesn't seem to sign webhooks, so you'll have to rely on either no one knowing your webhook address (bad), or IP whitelisting (a bit better). - The bot is designed to be used in one guild only.
Interview: When a Bot Gets Big
There are over a dozen websites for listing Discord bots and making them available to the public at large, including DiscordBots.org and Discord.Bots.gg. Although Discord bots are mostly the foray of small-time hobbyists, some bots experience tremendous popularity and maintaining them evolves into a complex and demanding job.
By guild-count, Rythm is currently the most widespread bot on Discord. Rythm is a music bot whose specialty is connecting to voice channels in Discord and playing music requested by users. Rythm is currently present in over 2,850,000 guilds containing a sum population of around 90 million users, and at its peak plays audio for around 100,000 simultaneous users in 20,000 separate guilds. Rythm's creator and main developer, ImBursting, kindly agreed to answer a few questions about what it's like to develop and maintain a large-scale bot like Rythm.
Interviewer: Can you tell us a bit about Rythm's high level architecture and how it's hosted?
ImBursting: Rythm is scaled across 9 physical servers, each have 32 cores, 96GB of RAM and a 10gbps connection. These servers are collocated at a data center with help from a small hosting company, GalaxyGate.
I imagine that when you started working on Rythm, you didn't design it to scale anywhere near as much as it has. Can you tell us about about how Rythm started, and its technical evolution over time?
Rythm's first evolution was written in Python, which isn't a very performant language, so around the time we hit 10,000 servers (after many scaling attempts) I realised this was the biggest roadblock and so I began recoding the bot to Java, the reason being Java's audio libraries were a lot more optimised and it was generally a better suited language for such a huge application. After re-coding, performance improved tenfold and kept the issues at bay for a while. And then we hit the 300,000 servers milestone when issues started surfacing again, at which point I realised that more scaling was required since one JVM just wasn't able to handle all that. So we slowly started implementing improvements and major changes like tuning the garbage collector and splitting voice connections onto separate microservices using an open source server called Lavalink. This improved performance quite a bit but the final round of infrastructure was when we split this into 9 seperate clusters to run on 9 physical servers, and made custom gateway and stats microservices to make sure everything ran smoothly like it would on one machine.
I noticed that Rythm has a canary version and you get some help from other developers and staff. I imagine you and your team must put a lot of effort into making sure things are done right. Can you tell us about what processes are involved in updating Rythm?
Rythm canary is the alpha bot we use to test freshly made features and performance improvements before usually deploying them to Rythm 2 to test on a wider scale and then production Rythm. The biggest issue we encounter is really long reboot times due to Discord rate limits, and is the reason I try my best to make sure an update is ready before deciding to push it.
I do get a lot of help from volunteer developers and people who genuinely want to help the community, I want to make sure everything is done correctly and that people will always get their questions answered and get the best support possible which means im constantly on the lookout for new opportunities.
Wrapping It Up
Discord's days of being a new kid on the block are past, and it is now one of the largest real-time communication platforms in the world. While Discord bots are largely the foray of small-time hobbyists, we may well see commercial opportunities increase as the population of the service continues to increase. Some companies, like the aforementioned Patreon, have already waded in.
In this article, we saw a high-level overview of Discord's user interface, a high-level overview of its APIs, a complete lesson in Discord bot programming, and we got to hear about what it's like to operate a bot at enterprise scale. I hope you come away interested in the technology and feeling like you understand the fundamentals of how it works.
Chatbots are generally fun, except when their responses to your intricate queries have the intellectual the depth of a cup of water. To ensure a great UX for your users see The Chat Crash - When a Chatbot Fails by the Toptal Design Blog for 5 design problems to avoid.