Discord 봇을 만드는 방법: 개요 및 자습서

게시 됨: 2022-03-11

Discord는 "게이머를 위한 올인원 음성 및 문자 채팅"으로 광고하는 실시간 메시징 플랫폼입니다. 매끄러운 인터페이스, 사용 용이성 및 광범위한 기능으로 인해 Discord는 빠른 성장을 경험했으며 비디오 게임에 거의 관심이 없는 사람들에게도 점점 인기를 얻고 있습니다. 2017년 5월과 2018년 5월 사이에 사용자 기반은 4,500만 사용자에서 1억 3,000만 명 이상으로 폭발적으로 증가했으며 일일 사용자는 Slack보다 두 배 이상 많습니다.

챗봇 개발자의 관점에서 Discord의 가장 매력적인 기능 중 하나는 Discord를 외부 세계와 통합하고 사용자에게 보다 매력적인 경험을 제공하는 데 도움이 되는 프로그래밍 가능한 봇에 대한 강력한 지원입니다. 봇은 Discord에서 어디에나 있으며 중재 지원, 게임, 음악, 인터넷 검색, 지불 처리 등을 포함한 광범위한 서비스를 제공합니다.

이 Discord 봇 자습서에서는 JavaScript로 간단한 Discord 봇을 작성하는 자습서로 이동하기 전에 Discord 사용자 인터페이스와 봇용 REST 및 WebSocket API에 대해 설명합니다. 마지막으로 특정 지표에 따라 Discord의 가장 인기 있는 봇과 중요한 인프라 및 코드베이스를 개발하고 유지 관리한 경험을 듣게 됩니다.

Discord 사용자 인터페이스

기술적인 세부 사항을 논의하기 전에 사용자가 Discord와 상호 작용하는 방식과 Discord가 사용자에게 표시되는 방식을 이해하는 것이 중요합니다. 봇에 표시되는 방식은 개념적으로 비슷합니다(물론 비시각적임). 사실, 공식 Discord 애플리케이션은 봇이 사용하는 것과 동일한 API를 기반으로 합니다. 기술적으로 약간의 수정으로 일반 사용자 계정 내에서 봇을 실행하는 것이 가능하지만 이는 Discord의 서비스 약관에 의해 금지되어 있습니다. 봇은 봇 계정에서 실행해야 합니다.

다음은 Chrome 내에서 실행되는 Discord 애플리케이션의 브라우저 버전 1 입니다.

디스코드 웹 UI

1 데스크톱 애플리케이션용 Discord UI는 Electron으로 패키징된 웹 애플리케이션과 거의 동일합니다. iOS 애플리케이션은 React Native로 빌드됩니다. Android 애플리케이션은 기본 Android Java 코드입니다.

분해해 봅시다.

1. 서버 목록

왼쪽에는 내가 구성원인 서버 목록이 있습니다. Slack에 익숙하다면 서버는 Slack 작업 공간과 유사하며 서버의 하나 이상의 채널 내에서 서로 상호 작용할 수 있는 사용자 그룹을 나타냅니다. 서버는 작성자 및/또는 그들이 선택하고 책임을 위임하기로 선택한 모든 직원이 관리합니다. 작성자 및/또는 직원은 규칙, 서버의 채널 구조를 정의하고 사용자를 관리합니다.

제 경우에는 Discord API 서버가 제 서버 목록의 맨 위에 있습니다. 도움을 받고 다른 개발자와 이야기를 나눌 수 있는 좋은 장소입니다. 그 아래에는 내가 만든 Test 라는 서버가 있습니다. 나중에 생성한 봇을 테스트할 것입니다. 그 아래에는 새 서버를 생성하는 버튼이 있습니다. 누구나 몇 번의 클릭으로 서버를 만들 수 있습니다.

Discord의 사용자 인터페이스에서 사용되는 용어는 Server 이지만 개발자 문서 및 API에서 사용되는 용어는 Guild 입니다. 기술적인 주제에 대한 이야기로 넘어가면 길드 에 대한 이야기로 넘어갈 것입니다. 두 용어는 서로 바꿔 사용할 수 있습니다.

2. 채널 목록

서버 목록 바로 오른쪽에는 현재 보고 있는 서버(이 경우 Discord API 서버)의 채널 목록이 있습니다. 채널은 임의의 수의 범주로 나눌 수 있습니다. Discord API 서버에서 카테고리는 그림과 같이 INFORMATION, GENERAL, LIBS를 포함합니다. 각 채널은 사용자가 채널 전용 주제에 대해 토론할 수 있는 대화방 역할을 합니다. 현재 보고 있는 채널(정보)의 배경이 더 밝습니다. 마지막으로 본 이후로 새 메시지가 있는 채널은 흰색 텍스트 색상으로 표시됩니다.

3. 채널 보기

현재 보고 있는 채널에서 사용자들이 어떤 이야기를 했는지 볼 수 있는 채널 보기입니다. 여기에서 부분적으로만 볼 수 있는 하나의 메시지를 볼 수 있습니다. 개별 Discord 봇 라이브러리에 대한 지원 서버에 대한 링크 목록입니다. 서버 관리자는 이 채널을 나와 같은 일반 사용자가 메시지를 보낼 수 없도록 구성했습니다. 관리자는 이 채널을 게시판으로 사용하여 쉽게 볼 수 있고 채팅에 빠져들지 않는 몇 가지 중요한 정보를 게시합니다.

4. 사용자 목록

오른쪽에는 이 서버에서 현재 온라인 상태인 사용자 목록이 있습니다. 사용자는 다른 범주로 구성되어 있으며 이름에는 색상이 다릅니다. 이것은 그들이 가진 역할 의 결과입니다. 역할은 사용자가 어떤 범주(있는 경우)에 나타나야 하는지, 이름 색상은 무엇이어야 하며 서버에서 어떤 권한을 가져야 하는지를 설명합니다. 사용자는 하나 이상의 역할을 가질 수 있으며(매우 자주 수행), 이 경우에 어떤 일이 발생하는지 결정하는 몇 가지 우선 순위 계산이 있습니다. 최소한 모든 사용자는 @everyone 역할을 갖습니다. 다른 역할은 서버 직원이 만들고 할당합니다.

5. 텍스트 입력

이것은 내가 허용된 경우 메시지를 입력하고 보낼 수 있는 텍스트 입력입니다. 이 채널에서 메시지를 보낼 수 있는 권한이 없기 때문에 여기에 입력할 수 없습니다.

6. 사용자

현재 사용자입니다. 나는 혼동을 피하기 위해 그리고 이름 선택에 서툴기 때문에 내 사용자 이름을 "나"로 설정했습니다. 내 사용자 이름 아래에는 내 판별자인 숫자(#9484)가 있습니다. "Me"라는 이름의 다른 사용자가 많이 있을 수 있지만 나는 "Me#9484"뿐입니다. 또한 서버별로 닉네임을 설정할 수 있으므로 서버마다 다른 이름으로 알려질 수 있습니다.

이것들은 Discord 사용자 인터페이스의 기본 부분이지만 그 외에도 훨씬 더 많습니다. 계정을 만들지 않고도 Discord를 쉽게 사용할 수 있으므로 잠시 시간을 내어 둘러보세요. Discord 홈페이지를 방문하여 "브라우저에서 Discord 열기"를 클릭하고 사용자 이름을 선택하고 "버스 사진 클릭"을 한 두 번 재생하면 Discord에 들어갈 수 있습니다.

디스코드 API

Discord API는 WebSocket과 REST API의 두 부분으로 구성됩니다. 일반적으로 WebSocket API는 Discord에서 실시간으로 이벤트를 수신하는 데 사용되며 REST API는 Discord 내부에서 작업을 수행하는 데 사용됩니다.

디스코드 봇 통신 루프 만드는 방법

웹소켓 API

WebSocket API는 메시지 생성, 메시지 삭제, 사용자 킥/밴 이벤트, 사용자 권한 업데이트 등을 포함하여 Discord에서 이벤트를 수신하는 데 사용됩니다. 반면에 봇에서 WebSocket API로의 통신은 더 제한적입니다. 봇은 WebSocket API를 사용하여 연결을 요청하고, 자신을 식별하고, 하트비트를 표시하고, 음성 연결을 관리하고, 몇 가지 더 기본적인 작업을 수행합니다. Discord의 게이트웨이 문서에서 자세한 내용을 읽을 수 있습니다(WebSocket API에 대한 단일 연결을 게이트웨이라고 함). 다른 작업을 수행하기 위해 REST API가 사용됩니다.

WebSocket API의 이벤트에는 이벤트 유형에 따라 달라지는 정보가 포함된 페이로드가 포함됩니다. 예를 들어 모든 Message Create 이벤트에는 메시지 작성자를 나타내는 사용자 개체가 수반됩니다. 그러나 사용자 개체만으로는 사용자에 대해 알아야 할 모든 정보가 포함되어 있지 않습니다. 예를 들어 사용자 권한에 대한 정보가 포함되어 있지 않습니다. 더 많은 정보가 필요한 경우 REST API를 쿼리할 수 있지만 다음 섹션에서 자세히 설명하는 이유 때문에 일반적으로 이전 이벤트에서 수신한 페이로드에서 구축했어야 하는 캐시에 액세스해야 합니다. Guild Create , Guild Role UpdateChannel Update 를 포함하되 이에 국한되지 않는 사용자 권한과 관련된 페이로드를 전달하는 여러 이벤트가 있습니다.

봇은 WebSocket 연결당 최대 2,500개의 길드에 존재할 수 있습니다. 봇이 더 많은 길드에 존재하도록 하려면 봇이 샤딩을 구현하고 Discord에 대한 여러 개의 개별 WebSocket 연결을 열어야 합니다. 봇이 단일 노드의 단일 프로세스 내에서 실행되는 경우 불필요한 것처럼 보일 수 있는 복잡성이 추가될 뿐입니다. 그러나 봇이 매우 인기 있고 별도의 노드에 백엔드를 분산해야 하는 경우 Discord의 샤딩 지원을 통해 이 작업을 훨씬 쉽게 수행할 수 있습니다.

REST API

Discord REST API는 봇이 메시지 전송, 사용자 추방/금지, 사용자 권한 업데이트(WebSocket API에서 수신한 이벤트와 대체로 유사)와 같은 대부분의 작업을 수행하는 데 사용됩니다. REST API를 사용하여 정보를 쿼리할 수도 있습니다. 그러나 봇은 주로 WebSocket API의 이벤트에 의존하고 WebSocket 이벤트에서 받은 정보를 캐시합니다.

여기에는 몇 가지 이유가 있습니다. 예를 들어, 메시지 생성 이벤트가 수신될 때마다 사용자 정보를 얻기 위해 REST API를 쿼리하는 것은 REST API의 속도 제한으로 인해 확장되지 않습니다. WebSocket API가 필요한 정보를 제공하고 캐시에 있어야 하므로 대부분의 경우 중복됩니다.

그러나 몇 가지 예외가 있으며 때때로 캐시에 없는 정보가 필요할 수 있습니다. 봇이 처음에 WebSocket 게이트웨이에 연결되면 봇이 현재 상태로 캐시를 채울 수 있도록 해당 샤드에 봇이 있는 길드당 하나의 Ready 이벤트와 하나의 Guild Create 이벤트가 초기에 봇으로 전송됩니다. 인구가 많은 길드를 위한 길드 생성 이벤트에는 온라인 사용자에 대한 정보만 포함됩니다. 봇이 오프라인 사용자에 대한 정보를 가져와야 하는 경우 캐시에 관련 정보가 없을 수 있습니다. 이 경우 REST API에 요청하는 것이 좋습니다. 또는 오프라인 사용자에 대한 정보를 자주 얻어야 하는 경우 대신 WebSocket API에 Request Guild Members opcode를 보내 오프라인 길드 회원을 요청할 수 있습니다.

또 다른 예외는 애플리케이션이 WebSocket API에 전혀 연결되어 있지 않은 경우입니다. 예를 들어 봇에 사용자가 로그인하여 서버에서 봇 설정을 변경할 수 있는 웹 대시보드가 ​​있는 경우. 웹 대시보드는 WebSocket API에 연결하지 않고 Discord의 데이터 캐시 없이 별도의 프로세스에서 실행할 수 있습니다. 가끔 몇 번의 REST API 요청만 하면 될 수도 있습니다. 이러한 종류의 시나리오에서는 REST API에 의존하여 필요한 정보를 얻는 것이 합리적입니다.

API 래퍼

기술 스택의 모든 수준을 이해하는 것은 항상 좋은 생각이지만 Discord WebSocket 및 REST API를 직접 사용하는 것은 시간이 많이 걸리고 오류가 발생하기 쉽고 일반적으로 불필요하고 실제로 위험합니다.

Discord는 공식적으로 검증된 라이브러리의 선별된 목록을 제공하고 다음을 경고합니다.

API를 남용하거나 과도한 속도 제한을 일으키는 사용자 지정 구현 또는 비준수 라이브러리를 사용하면 영구적으로 금지될 수 있습니다.

Discord에서 공식적으로 검증한 라이브러리는 일반적으로 성숙하고 문서화되어 있으며 Discord API의 전체 범위를 제공합니다. 대부분의 봇 개발자는 호기심이나 용기를 제외하고 사용자 정의 구현을 개발할 타당한 이유가 없습니다!

현재 공식적으로 검증된 라이브러리에는 Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust 및 Swift에 대한 구현이 포함됩니다. 선택한 언어에 대해 두 개 이상의 다른 라이브러리가 있을 수 있습니다. 사용할 것을 선택하는 것은 어려운 결정일 수 있습니다. 해당 문서를 확인하는 것 외에도 비공식 Discord API 서버에 가입하여 각 라이브러리 뒤에 어떤 종류의 커뮤니티가 있는지 느낄 수 있습니다.

Discord 봇을 만드는 방법

본론으로 들어가자. 우리는 서버에 매달려 Ko-fi의 웹훅을 수신하는 Discord 봇을 만들 것입니다. Ko-fi는 PayPal 계정으로 쉽게 기부를 수락할 수 있는 서비스입니다. 비즈니스 계정이 필요한 PayPal과 달리 웹훅 설정이 매우 간단하여 데모 목적이나 소규모 기부 처리에 적합합니다.

사용자가 $10 이상을 기부하면 봇은 이름 색상을 변경하고 온라인 사용자 목록의 맨 위로 이동시키는 Premium Member 역할을 할당합니다. 이 프로젝트에서는 Node.js와 Eris라는 Discord API 라이브러리를 사용할 것입니다(문서 링크: https://abal.moe/Eris/). Eris는 유일한 JavaScript 라이브러리가 아닙니다. 대신 discord.js를 선택할 수 있습니다. 우리가 작성할 코드는 어느 쪽이든 매우 유사합니다.

한편, 또 다른 기부 처리자인 Patreon은 공식 Discord 봇을 제공하고 Discord 역할을 기여자 혜택으로 구성할 수 있도록 지원합니다. 우리는 유사하지만 더 기본적인 것을 구현할 것입니다.

튜토리얼의 모든 단계에 대한 코드는 GitHub(https://github.com/mistval/premium_bot)에서 사용할 수 있습니다. 이 게시물에 표시된 단계 중 일부는 간결함을 위해 변경되지 않은 코드를 생략하므로 누락된 것이 있다고 생각되면 제공된 GitHub 링크를 따르십시오.

봇 계정 만들기

코드 작성을 시작하기 전에 봇 계정이 필요합니다. 봇 계정을 생성하기 전에 사용자 계정이 필요합니다. 사용자 계정을 만들려면 여기의 지침을 따르세요.

그런 다음 봇 계정을 만들기 위해 다음을 수행합니다.

1) 개발자 포털에서 애플리케이션을 생성합니다.

개발자 포털의 스크린샷

2) 응용 프로그램에 대한 몇 가지 기본 세부 정보를 입력합니다(여기에 표시된 CLIENT ID를 기록해 둡니다. 나중에 필요합니다).

기본 세부정보 입력 스크린샷

3) 애플리케이션에 연결된 봇 사용자를 추가합니다.

봇 사용자 추가 스크린샷

4) PUBLIC BOT 스위치를 끄고 표시된 봇 토큰을 확인합니다(나중에 이 토큰도 필요함). 예를 들어 Toptal 블로그 게시물의 이미지로 게시하여 봇 토큰을 유출한 경우 즉시 재생성해야 합니다. 봇 토큰을 소유한 사람은 누구나 봇의 계정을 제어할 수 있으며 귀하와 사용자에게 잠재적으로 심각하고 영구적인 문제를 일으킬 수 있습니다.

"와일드 봇이 나타났습니다"의 스크린샷

5) 테스트 길드에 봇을 추가합니다. 길드에 봇을 추가하려면 클라이언트 ID(앞서 표시)를 다음 URI로 대체하고 브라우저에서 탐색합니다.

https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX

테스트 길드에 봇 추가

승인을 클릭하면 봇이 이제 내 테스트 길드에 있고 사용자 목록에서 볼 수 있습니다. 오프라인이지만 곧 수정하겠습니다.

프로젝트 생성

Node.js가 설치되어 있다고 가정하고 프로젝트를 만들고 Eris(우리가 사용할 봇 라이브러리), Express(웹훅 수신기를 만드는 데 사용할 웹 애플리케이션 프레임워크) 및 body-parser(웹훅 본문 구문 분석용)를 설치합니다. ).

 mkdir premium_bot cd premium_bot npm init npm install eris express body-parser

봇을 온라인 상태로 만들고 반응형으로 만들기

아기 단계부터 시작하겠습니다. 먼저 봇을 온라인 상태로 만들고 응답합니다. 10-20줄의 코드로 이 작업을 수행할 수 있습니다. 새 bot.js 파일 내부에서 Eris 클라이언트 인스턴스를 생성하고 봇 토큰(위에서 봇 애플리케이션을 생성할 때 획득)을 전달하고 클라이언트 인스턴스의 일부 이벤트를 구독하고 Discord에 연결하도록 지시해야 합니다. . 데모 목적으로 봇 토큰을 bot.js 파일에 하드코딩 하지만 별도의 구성 파일을 만들고 소스 제어에서 제외하는 것이 좋습니다.

(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();

모든 것이 잘되면 이 코드를 자신의 봇 토큰으로 실행하면 Connected and ready. 콘솔에 인쇄되고 테스트 서버에서 봇이 온라인 상태가 되는 것을 볼 수 있습니다. 2 봇을 마우스 오른쪽 버튼으로 클릭하고 "멘션"을 선택하거나 이름 앞에 @를 입력하여 봇을 멘션할 수 있습니다. 봇은 "Present"라고 응답해야 합니다.

봇이 있습니다

2 멘션은 다른 사용자가 없는 경우에도 주의를 끌 수 있는 방법입니다. 일반 사용자가 언급되면 데스크탑 알림, 모바일 푸시 알림 및/또는 시스템 트레이의 Discord 아이콘 위에 나타나는 작은 빨간색 아이콘으로 알림을 받습니다. 사용자에게 알림을 받는 방식은 설정과 온라인 상태에 따라 다릅니다. 반면에 봇은 언급될 때 특별한 알림을 받지 않습니다. 다른 메시지와 마찬가지로 일반 Message Create 이벤트를 수신하고 이벤트에 첨부된 멘션을 확인하여 멘션되었는지 확인할 수 있습니다.

지불 명령 기록

이제 봇을 온라인으로 가져올 수 있다는 것을 알았으므로 현재 Message Create 이벤트 핸들러를 제거하고 사용자로부터 지불을 받았음을 봇에 알릴 수 있는 새 이벤트 핸들러를 생성해 보겠습니다.

결제 봇에 알리기 위해 다음과 같은 명령을 실행합니다.

 pb!addpayment @user_mention payment_amount

예를 들어, pb!addpayment @Me 10.00 은 Me가 지불한 $10.00을 기록합니다.

피비! 부분을 ​​명령 접두사라고 합니다. 봇에 대한 모든 명령이 시작해야 하는 접두사를 선택하는 것이 좋습니다. 이렇게 하면 봇에 대한 네임스페이스 측정값이 생성되고 다른 봇과의 충돌을 방지하는 데 도움이 됩니다. 대부분의 봇에는 도움말 명령이 포함되어 있지만 길드에 10개의 봇이 있고 모두 도움 에 응답했다면 혼란을 상상해 보십시오! PB를 사용하여! as는 동일한 접두사를 사용하는 다른 봇이 있을 수 있으므로 완벽한 솔루션이 아닙니다. 가장 인기 있는 봇은 충돌을 방지하기 위해 길드별로 접두사를 구성할 수 있습니다. 또 다른 옵션은 봇 자체 언급을 접두사로 사용하는 것입니다. 하지만 이렇게 하면 실행 명령이 더 장황해집니다.

(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();

해 보자.

봇과 상호 작용

봇이 pb!addpayment 명령에 응답하도록 할 뿐만 아니라 명령 처리를 위한 일반화된 패턴을 만들었습니다. commandHandlerForCommandName 사전에 더 많은 핸들러를 추가하여 더 많은 명령을 추가할 수 있습니다. 여기에 간단한 명령 프레임워크가 있습니다. 명령 처리는 많은 사람들이 직접 작성하는 대신 사용할 수 있는 오픈 소스 명령 프레임워크를 작성하고 봇을 만드는 기본적인 부분입니다. 명령 프레임워크를 사용하면 종종 쿨다운, 필요한 사용자 권한, 명령 별칭, 명령 설명 및 사용 예(자동으로 생성된 도움말 명령의 경우) 등을 지정할 수 있습니다. Eris는 기본 제공 명령 프레임워크와 함께 제공됩니다.

권한에 대해 말하자면, 우리 봇에는 약간의 보안 문제가 있습니다. 누구나 addpayment 명령을 실행할 수 있습니다. 봇 소유자만 사용할 수 있도록 제한합시다. commandHandlerForCommandName 사전을 리팩터링하고 JavaScript 개체를 값으로 포함하도록 합니다. 이러한 개체에는 명령 처리기가 있는 execute 속성과 부울 값이 있는 botOwnerOnly 속성이 포함됩니다. 또한 봇의 소유자가 누구인지 알 수 있도록 사용자 ID를 봇의 상수 섹션에 하드코딩합니다. Discord 설정에서 개발자 모드를 활성화한 다음 사용자 이름을 마우스 오른쪽 버튼으로 클릭하고 ID 복사를 선택하여 사용자 ID를 찾을 수 있습니다.

(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();

이제 봇 소유자가 아닌 다른 사람이 명령을 실행하려고 하면 봇은 addpayment 명령 실행을 화나게 거부합니다.

다음으로 봇이 10달러 이상을 기부하는 사람에게 Premium Member 역할을 할당하도록 합시다. bot.js 파일의 상단 부분:

(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), ]); }, };

이제 pb!addpayment @Me 10.00 봇이 나에게 Premium Member 역할을 할당해야 합니다.

죄송합니다. 콘솔에 권한 누락 오류가 나타납니다.

 DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013

봇은 테스트 길드에서 역할 관리 권한이 없으므로 역할을 생성하거나 할당할 수 없습니다. 봇에게 관리자 권한을 부여할 수 있으며 이러한 종류의 문제는 다시는 발생하지 않습니다. 하지만 모든 시스템과 마찬가지로 사용자(또는 이 경우 봇)에게 필요한 최소 권한만 부여하는 것이 가장 좋습니다.

서버 설정에서 역할을 생성하고 해당 역할에 대한 역할 관리 권한을 활성화하고 봇에 역할을 할당하여 봇에 역할 관리 권한을 부여할 수 있습니다.

역할 관리 시작

새 역할 만들기

이제 명령을 다시 실행하려고 하면 역할이 생성되어 나에게 할당되고 멋진 이름 색상과 구성원 목록에서 특별한 위치가 있습니다.

새로운 역할이 할당됨

명령 처리기에는 잘못된 인수를 확인해야 한다고 제안하는 TODO 주석이 있습니다. 이제 처리해 보겠습니다.

(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), ]); }, };

지금까지의 전체 코드는 다음과 같습니다.

(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();

이것은 Discord 봇을 만드는 방법에 대한 좋은 기본 아이디어를 제공합니다. 이제 봇을 Ko-fi와 통합하는 방법을 살펴보겠습니다. 원하는 경우 Ko-fi의 대시보드에서 웹훅을 만들고 라우터가 포트 80을 전달하도록 구성되어 있는지 확인하고 실제 라이브 테스트 웹훅을 자신에게 보낼 수 있습니다. 하지만 저는 Postman을 사용하여 요청을 시뮬레이션할 것입니다.

Ko-fi의 Webhook은 다음과 같은 페이로드를 제공합니다.

 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" }

webhook_listener.js라는 새 소스 파일을 만들고 Express를 사용하여 webhook을 수신 대기해 보겠습니다. 우리는 단 하나의 Express 경로를 가질 것이며 이것은 데모용이므로 관용적 디렉토리 구조를 사용하는 것에 대해 너무 걱정하지 않을 것입니다. 우리는 모든 웹 서버 로직을 하나의 파일에 넣을 것입니다.

(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;

그런 다음 bot.js를 실행할 때 리스너가 시작되도록 bot.js 상단에 새 파일이 필요하도록 합시다.

(GitHub 코드 링크: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)

 const eris = require('eris'); const webhookListener = require('./webhook_listener.js');

봇을 시작한 후 브라우저에서 http://localhost/kofi로 이동하면 "Hello"가 표시되어야 합니다.

이제 WebhookListener 가 webhook의 데이터를 처리하고 이벤트를 내보내도록 합시다. 이제 브라우저가 경로에 액세스할 수 있는지 테스트했으므로 Ko-fi의 웹훅이 POST 요청이 되기 때문에 경로를 POST 경로로 변경해 보겠습니다.

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

Postman으로 테스트

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.

Two nice messages

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

다음 단계

We've successfully created a bot that can help us track donations. Is this something we can actually use? 글쎄, 아마도. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.

마무리

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.

관련 항목: JS 모범 사례: TypeScript 및 종속성 주입을 사용하여 Discord 봇 빌드