디스코드 라이브러리인 Discord.js를 이용하여 디스코드 봇을 만들어보기로 했습니다.

게임동호회에서 직접 사용할 랜덤 팀짜기 봇을 만들 예정입니다. ㅎㅎ

 

22.07.09 추가
- 이하에서 웹훅(webhook) 관련해서 나오는 내용들은, 만약 웹훅 기능을 사용하실 것이 아니면 스킵하셔도 무방합니다!

 

준비

- Discord.js 설치하기

npm i discord.js --save

- 팀짜기 로직

  - 게임에 참가하는 멤버들의 명단을 배열로 받은 뒤, 각 멤버들에게 임의의 key 값(Math.random 사용)을 부여합니다.

  - key 값의 크기에 따라 오름차순으로 요소를 정렬한 뒤 팀원 수에 맞게 배열을 나눕니다.

 

- 튜토리얼 참고하기

  - 제스퍼(Ukong0324)님의 Discord-JS-Tutorial을 참고하기로 했습니다.

 

첫째 날

- 튜토리얼에 따라 애플리케이션을 생성하고, 봇을 추가했습니다.

그림판 3D로 만든 생선을 봇의 프사로 설정해주었습니다.

- 테스트를 위해, 개인 서버에서 웹훅을 만들었습니다.

서버 이름 클릭 > 서버 설정 > 연동 > 웹후크 만들기
사진, 이름, 채널을 설정해주세요.

  - '웹후크 URL 복사'를 클릭하시면 아래와 같이 웹훅의 URL이 복사 되는데요, 이 URL에는 표시한 바와 같이 웹훅의 ID와 토큰이 포함되어 있습니다.

 

- 아까 봇을 만들었던 페이지에서 나가지 마시고 아래 표시한 Copy 버튼을 클릭하여 봇의 토큰을 복사해주세요.

- 튜토리얼의 bot.js를 참고하여 테스트용 코드를 작성해보았습니다.

const Discord = require('discord.js'); // Discord.js 모듈 불러오기
const client = new Discord.Client(); // 이 Client는 Discord.js에서 핵심적으로 쓰이는 존재(?)라고 합니다.
const prefix = '!'; // 봇을 호출할 때 사용할 prefix를 설정합니다.
require('dotenv').config();

client.on('ready', () => { // client에 ready 이벤트가 발생하면 아래의 내용 출력하기
  console.log(`${client.user.tag} 봇에 로그인 했습니다!`);
});
// cf.) on과 달리 once는 이벤트를 한 번만 듣는다고 하네요.

client.on('message', msg => { // client에 message 이벤트가 발생할 경우의 동작 지정
  if (!msg.guild) return; // guild 이외의 곳에선 작동하지 않도록 설정
  if (msg.author.bot) return; // 메시지 사용자가 봇일 경우 작동하지 않도록 설정
  if (msg.content.indexOf(prefix) !== 0) return; // 메시지가 prefix(!)로 시작되지 않는 경우 작동하지 않도록 설정

  const args = msg.content.slice(prefix.length).trim().split(/ +/g);
  /**
   * prefix를 짤라주고, 끝의 공백이 있다면 제거해준 뒤,
   * 메시지(문자열)를 띄어쓰기를 기준으로 나누어서 배열에 담아주기
   * 이때 가장 첫 번째 요소가 명령어가 되겠죠!?
   * 
   * ex) !kick Anne => ['kick', 'Anne']
   */

  const command = args.shift().toLowerCase();
  /**
   * 명령어 가져오기
   * shift는 원본 배열을 변환하는 메서드로서,
   * 원본 배열에서 첫 번째 요소를 제거하고 제거한 요소를 반환합니다.
   * 즉, 이 경우에는 명령어가 반환될테고 그 값이 곧 command라는 변수에 바인딩 되겠죠!?
   * toLowerCase의 경우 사용자가 실수로 대문자를 입력한 경우에
   * 이를 소문자로 변환해주기 위해 작성합니다.
   */

  if (command === 'ping') { // 'ping'이라는 명령어를 받으면,
    msg.reply(`${client.ws.ping}ms`); // 웹소켓(websocket) 지연 시간을 알려주기
  }

  if (command === 'embed') {
    const embed = new Discord.MessageEmbed()
      .setTitle('여기는 대표 타이틀!') // 임베드에서 타이틀로 사용된다고 하네요.
      .setDescription('여기는 대표 설명!') // 타이틀을 설명해줍니다.
      .setColor('DARK_GOLD') // 색상을 설정합니다.
      .setFooter('여기는 푸터!') // 푸터를 설정합니다.
      .setThumbnail('http://blogfiles.naver.net/20151023_23/shin_0305_1445573936921jrPRT_JPEG/%BD%E6%B3%D7%C0%CF%BF%B9%BD%C3.jpg') // 썸네일로 사용될 이미지를 설정합니다.
      .setImage('https://github.com/TEAM-SUITS/Suits/raw/develop/client/public/assets/og-image.jpg?raw=true') // 임베드에서 사용될 이미지를 설정합니다.
      .setTimestamp() // 인자를 전달하지 않으면 현재 시각을 찍어냅니다.
      .addField('여기는 소제목', '여기는 설명'); // 첫 번째 인자로는 소제목을, 두 번째 인자로는 설명을 전달합니다.
    
      msg.reply(embed);
      // embed를 답변 내용으로서 전달하도록 합니다.
  }

  if (command === 'webhook') {
    const hook = new Discord.WebhookClient(
      process.env.WEBHOOK_ID,
      process.env.WEBHOOK_TOKEN
    );
    // 첫 번째 인자로는 앞서 만들었던 웹훅 ID를 전달해주시고,
    // 두 번째 인자로는 웹훅 토큰을 전달해주세요. (토큰의 경우 보안에 주의하세요!!)
    
    hook.send('Hello, new world!');
    // 지정되어있는 채널에 웹훅이 이 내용을 보내줍니다.
  }

  /* ----------------- 관리자 기능(Moderator) ---------------- */
  if (command === 'kick') { // 추방 명령
    const user = msg.mentions.users.first();

    if (!user) {
      msg.reply("추방하시기 전에 맨션을 먼저 해주세요!");
    } else {
      const member = msg.guild.member(user);

      if (member) {
        member.kick(`${msg.author.username}님에 의해 서버에서 추방됨.`) // audit log에 추방 내용 로그 남기기
          .then(member => {
            msg.reply(`성공적으로 ${member.user.tag}님을 추방하였습니다.`);
            // 채팅 친 곳에 해당 유저의 추방 내용 알리기
          })
          .catch(console.error);
      } else { // member가 없다면,
        msg.reply('이 서버에 존재하지 않는 유저입니다.');
      }
    }
  }

  if (command === 'block') { // 차단 명령
    const user = msg.mentions.users.first();

    if (!user) {
      msg.reply("차단하시기 전에 맨션을 먼저 해주세요!");
    } else {
      const member = msg.guild.member(user);

      if (member) {
        member.ban(`${msg.author.username}님에 의해 서버에서 차단됨.`) // audit log에 차단 내용 로그 남기기
          .then(member => {
            msg.reply(`성공적으로 ${member.user.tag}님을 차단하였습니다.`)
            // 채팅 친 곳에 해당 유저의 차단 내용 알리기
          })
          .catch(console.error);
      } else { // member가 없다면,
        msg.reply('이 서버에 존재하지 않는 유저입니다.');
      }
    }
  }

  if (command === 'clean') { // 메시지 청소 명령 (bulk delete)
    if (!args[0]) return msg.reply('청소할 만큼의 값을 정수로 적어주세요.');
    if (!Number(args[0])) return msg.reply('메시지를 지울 값으로는 반드시 숫자를 전달해주세요.');
    // Number는 인자로 전달한 값이 숫자로 암묵적 변환할 수 없는 값일 때 NaN을 반환합니다.
    if (args[0] < 1) return msg.reply('메시지를 지울 값은 1보다 커야 합니다.');
    if (args[0] > 100) return msg.reply('메시지를 지울 값은 100보다 작아야 합니다.');

    msg.channel.bulkDelete(args[0])
      .then(msg.reply(`${args[0]}만큼의 메시지를 성공적으로 삭제했습니다.`))
      .catch(console.error);
  }
});

client.login(process.env.BOT_TOKEN);
// 위에서 복사해두셨던 봇의 토큰을 인자로서 전달해주세요.
// 웹훅 토큰과 마찬가지로 보안에 주의하세요!!

- 웹훅 ID 및 토큰의 경우에는 보안에 주의하셔야 하니 인자로 바로 전달하시는 것 보다는, 아래 포스팅 참고하셔서 dotenv로 관리하시길 권장드립니다.

  - www.daleseo.com/js-dotenv/

 

dotenv로 환경 변수 관리하기

Engineering Blog by Dale Seo

www.daleseo.com

  - 저의 경우 GitHub 원격 저장소에 코드를 저장해두기 때문에, 원격 저장소에 코드를 올릴 때 실수로 토큰 정보를 레포에 올리는 일을 방지하기 위해 .gitignore 파일에 .env를 미리 추가해두었습니다.

 

- 코드를 실행시켜서 잘 돌아가는지 확인해볼까요?

오호라~ 잘 돌아가네요~

 

- 서버에 봇을 추가해서 테스트해봅시다. (참고)

  1. 아까 봇을 생성한 페이지에서, 좌측의 OAuth2 메뉴를 클릭합니다.

  2. Scopes 항목 중 bot에 체크를 해주세요.

  3. 하단에서 봇 권한(BOT PERMISSIONS)을 설정해주시고, 다시 Scopes 부분에서 아래에 표시한 링크를 복사해주세요. 봇 초대 링크입니다!

  4. 복사하신 링크를 브라우저에 붙여넣으면 아래와 같은 화면이 뜰 텐데요, 봇을 추가하시려는 서버를 선택해주시고 '계속하기'를 눌러주세요. 그리고 봇의 권한을 다시 한번 확인하시고 '승인'을 눌러주세요.

참고로, 봇을 초대하시려는 서버에 대한 관리 권한이 있으셔야 합니다.

   5. 봇이 초대된 것을 확인하실 수 있습니다~

안녕 티미~

- 명령어 테스트!

 

오~ 똑똑하네요~

 

- 내일은 튜토리얼 3화부터 따라해볼 예정입니다. ㅎㅎ

 

복사했습니다!