Discord bot with Javascript(node.js)

가끔 지인들이랑 디스코드로 게임을 하는데 사람이 너무 많으면 편가르기를 하는데
그때마다 지인들은 네이버 사다리를 이용하고 이를 스트리밍해서 정보를 공유한다

근데 나는 엄청난 귀차니즘의 소유자라 사실 스트리밍에 들어가는 것을 보는 것 조차 귀찮다
그래서 디스코드 봇 중에서 팀나누기 봇이 있는지 검색해보는 과정에서 한글화된 봇이 없다는 점과 디스코드를 구축한 언어 중 하나가 Javascript라는 얘기를 들어본적이 있어서
이참에 한글화 된 봇을 만들어보면 어떨까해서 이렇게 글을 쓴다

먼저 이번 글에서는 기본적인 디스코드 봇의 개념과 디스코드 API를 어떻게 활용할 수 있는지 등에 대해서 알아보기 위해 일단 유튜브 강의를 통해 기본적인 튜토리얼을 해보자

먼저 Discord documentation에 접속해서 오른쪽 상단의 New Application을 클릭해보자
그러면 봇의 이름을 정하라고 할텐데 강의 내용대로 나는 Encourage Bot이라고 하겠다
생성을 하고 나면 왼쪽에서Bot탭을 클릭한 후 Add Bot을 클릭해서 봇을 추가하도록 하자

Authorization Flow에서 OAuth2(Open Authentication 2)란 페이스북, 카카오 등에서 제공하는 인증을 위한 표준 프로토콜이다

우리는 공개 봇으로 되있는 기본설정을 유지한 채로 진행하도록 하자
TOKEN이 있는데 이 TOKEN이 내 봇의 비밀번호가 될 것이다
TOKEN을 통해 내가 만든 디스코드 봇, 내 서버에 접속할 수 있기때문에 타인에게 공개하지 말도록 하자

만약 실수로 어딘가에 공개했다면 Regenerate를 눌러 새로 발급받기로 하자

이제 서버에 봇을 불러오기 위해 왼쪽에서 Oauth2탭을 눌러 SCOPES에서 bot을 체크해주고 BOT PERMISSIONS에서 원하는 권한을 선택해주자
나는 강의대로 TEXT PERMISSIONS에서 Send TTS Messages를 제외한 모든 것을 선택해주고 GENERAL PERMISSIONS에서 View Channels를 선택해주겠다

원하는 권한을 다 선택했다면 SCOPES 가장 하단에 있는 주소를 Copy버튼을 눌러 복사하도록 하자
이 복사된 주소로 접속하게 되면

위와 같은 화면이 나올텐데 ADD TO SERVER:에서 원하는 서버를 선택한 후 계속하기를 눌러주고 아까 PERMISSIONS에서 선택했던 권한들을 그대로 둔 채 승인버튼을 눌러 이 봇을 실제 디스코드 서버에 추가해주도록 하자

이제 코딩을 해볼텐데 코딩에 앞서서 나는 이번 프로젝트에서 repl.it을 사용하길 추천한다
repl.it은 설치없이 IDE환경을 구축해주며 각종 언어들도 설치없이 쉽게 사용이 가능하다
그리고 local server를 이용하면 내 컴퓨터를 계속 켜줘야되는데 repl.it을 통해 내가 컴퓨터를 종료해도 디스코드 봇은 계속 실행되도록 서버를 구축할 수 있기 때문이다

repl.it에 대한 사용법을 모르는 분들은 간단히 찾아보길 바란다
우리는 javascript를 이용해서 디스코드 봇을 만들 것이기 때문에 new repl을 클릭하고 언어는 node.js를 선택, 이름은 자신이 원하는 이름을 지어주면 된다

먼저 우리는 index.js라는 파일에 디스코드 라이브러리를 추가하도록 하자

const Discord = require('discord.js');

로컬환경이었다면 직접 라이브러리를 설치한 후 불러와야했겠지만 repl.itRun버튼을 누르면 설치되지 않은 라이브러리들이 자동으로 설치를 해준다는 점이 매력적이다
가끔 에러가 발생하면 Shell탭에 들어가서 직접 설치를 하는 방법으로 해결해주면 된다

메세지에 응답하는 아주 간단한 봇을 구축해보면서 디스코드 봇의 구조를 살펴보자

먼저 repl.it에서 제공하는 .env 즉 환경변수를 사용하는데
왼쪽 탭에서 자물쇠 모양의 secret을 선택한 후 key, value를 설정해주면 된다

const Discord = require('discord.js');
const client = new Discord.Client(); // discord 서버와 연결
const TOKEN = process.env['TOKEN'] // repl.it에서 제공하는 .env 변수 사용

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`)
});

client.on('message', msg => {
    if (msg.content === `hello`) {
        msg.reply(`hello ! my name is ${client.user.tag}!`)
    }
});

client.login(TOKEN)

위와 같이 작성한 후 Run을 누르면

정상적으로 코드가 작동된 것을 볼 수 있고

디스코드 상에서도 봇이 온라인 상태가 된 것을 알 수 있다

위에서 메세지가 만약 hello라면 대답을 하기로 되어있으므로 다른 메세지에는 대답을 하지 않다가 hello에 지정된 답변을 해주는 모습이다

이제 https://zenquotes.io/api를 통해 명언을 return해주는 함수를 만들어 이를 봇이 답변해주도록 해보자

const Discord = require('discord.js');
const client = new Discord.Client(); // discord 서버와 연결
const TOKEN = process.env['TOKEN'] // repl.it에서 제공하는 .env 변수 사용
const fetch = require('node-fetch');

function getQuote() {
    return fetch('https://zenquotes.io/api/random')
        .then(res => {
            return res.json();
        })
        .then(data => {
            return data[0]['q'] + ' -' + data[0]['a']; // q = quote, a = author
        })
}

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`)
});

client.on('message', msg => {
    if (msg.author.bot) return;

    if (msg.content === '$inspire') {
        getQuote().then(quote => msg.channel.send(quote))
    }
});

client.login(TOKEN)


Run버튼으로 실행하고 디스코드 서버로가서 채팅을 하게되면 위와 같이 대답을 해준다

비동기 호출에 관해 개념적으로 어려운 부분 혹은 이해가 안되는 부분이 있다면 최하단 reference을 참고하도록 하자

이제 우리는 지정된 단어에 대한 지정된 대답을 할 수 있는 기능을 넣어보도록 하겠다
강의에서는 encourage bot이라는 이름처럼 격려해주는 봇을 만들어보도록 하자

const Discord = require('discord.js');
const client = new Discord.Client(); // discord 서버와 연결
const TOKEN = process.env['TOKEN'] // repl.it에서 제공하는 .env 변수 사용
const fetch = require('node-fetch');

const sadWords = ['sad', 'depressed', 'unhappy', 'angry'];
const encouragements = [
    'Cheer up !',
    'Hang in there',
    'You are a great person !'
];

function getQuote() {
    return fetch('https://zenquotes.io/api/random')
        .then(res => {
            return res.json(); 
        })
        .then(data => {
            return data[0]['q'] + ' -' + data[0]['a']; // q = quote, a = author, zenquotes api의 문법
        })
}

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`)
});

client.on('message', msg => {
    if (msg.author.bot) return; // 채팅한 사람이 봇일 경우 대답하지 않음

    if (msg.content === '$inspire') {
        getQuote().then(quote => msg.channel.send(quote))
    }

    // sadWords안에 있는 곳을 살펴 본 후 msg에 word가 포함되어 있으면 실행
    if (sadWords.some(word => msg.content.includes(word))) {
        const RANDOM_NUM = Math.floor(Math.random() * encouragements.length);
        const encouragement = encouragements[RANDOM_NUM];

        msg.reply(encouragement);
    }
});

client.login(TOKEN)

지정된 단어가 포함된 문장을 입력하면 위와 같이 정해진 선택지에서 랜덤하게 대답을 해준다

왼쪽에서 database탭을 눌러서 Import the database아래 insert버튼을 눌러서 database를 불러오자
그 후 그 database에 우리의 encouragements배열을 기본값으로 설정하고 추가, 삭제할 수 있는 기능을 넣어보자

const Discord = require('discord.js');
const fetch = require('node-fetch');
const Database = require("@replit/database");


const client = new Discord.Client(); // discord 서버와 연결
const db = new Database();

const TOKEN = process.env['TOKEN'] // repl.it에서 제공하는 .env 변수 사용

const sadWords = ['sad', 'depressed', 'unhappy', 'angry'];
const defaultEncouragements = [
    'Cheer up !',
    'Hang in there',
    'You are a great person !'
];

//database에 encouragements(key)로 value를 불러옴
// 만약 NULL or length가 0이면 defaultEncouragements를 encouragements의 value로 설정
db.get('encouragements').then(encouragements =>  {
    // 처음은 NULL이지만 나중엔 length가 0일수도 있기 때문에
    if (!encouragements || encouragements.length < 1) {
        db.set('encouragements', defaultEncouragements)
    }
});

function updateEncouragements(encouragingMessage) {
    db.get('encouragements').then(encouragements => {
        encouragements.push([encouragingMessage]);

        db.set('encouragements', encouragements);
    });
};

function deleteEncouragement(index) {
    db.get('encouragements').then(encouragements => {
        if (encouragements.length > index) {
            encouragements.slice(index, 1);

            db.set('encouragements', encouragements);
        }

    });
}

function getQuote() {
    return fetch('https://zenquotes.io/api/random')
        .then(res => {
            return res.json(); 
        })
        .then(data => {
            return data[0]['q'] + ' -' + data[0]['a']; // q = quote, a = author, zenquotes api의 문법
        })
};

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`)
});

client.on('message', msg => {
    if (msg.author.bot) return; // 채팅한 사람이 봇일 경우 대답하지 않음

    if (msg.content === '$inspire') {
        getQuote().then(quote => msg.channel.send(quote))
    }

    // sadWords안에 있는 곳을 살펴 본 후 msg에 word가 포함되어 있으면 실행
    if (sadWords.some(word => msg.content.includes(word))) {
        db.get('encouragements').then(encouragements=> {
            const encouragement = encouragements[Math.floor(Math.random() * encouragements.length)];

            msg.reply(encouragement);
        })
    }

    if (msg.content.startsWith('$new')) {
        encouragingMessage = msg.content.split('$new')[1]; //'$new'가 0번째 element가 됨
        updateEncouragements(encouragingMessage);
        
        msg.channel.send('New encouraging message added.');
    }

    if (msg.content.startsWith('$del')) {
        index = parseInt(msg.content.split('$del')[1]);
        deleteEncouragement(index);
        
        msg.channel.send('encouraging message deleted.');
    }
});

client.login(TOKEN);

위와 같이 이제 $new$del 명령어를 추가하여 사용할 수 있게 되었다

이제 봇의 on/off를 ture, false로 키고 끌 수 있게끔 하는 명령어와 encouragements안에 있는 encouragingMessage의 list를 출력하는 명령어를 추가해보도록 하자

const Discord = require('discord.js');
const fetch = require('node-fetch');
const Database = require("@replit/database");


const client = new Discord.Client(); // discord 서버와 연결
const db = new Database();

const TOKEN = process.env['TOKEN'] // repl.it에서 제공하는 .env 변수 사용

const sadWords = ['sad', 'depressed', 'unhappy', 'angry'];
const defaultEncouragements = [
    'Cheer up !',
    'Hang in there',
    'You are a great person !'
];

//database에 encouragements(key)로 value를 불러옴
// 만약 NULL or length가 0이면 defaultEncouragements를 encouragements의 value로 설정
db.get('encouragements').then(encouragements =>  {
    // 처음은 NULL이지만 나중엔 length가 0일수도 있기 때문에
    if (!encouragements || encouragements.length < 1) {
        db.set('encouragements', defaultEncouragements)
    }
});

db.get('responding').then(value => {
    if (value == null) {
        db.set('responding', true);
    }
})

function updateEncouragements(encouragingMessage) {
    db.get('encouragements').then(encouragements => {
        encouragements.push([encouragingMessage]);

        db.set('encouragements', encouragements);
    });
};

function deleteEncouragement(index) {
    db.get('encouragements').then(encouragements => {
        if (encouragements.length > index) {
            encouragements.slice(index, 1);

            db.set('encouragements', encouragements);
        }

    });
}

function getQuote() {
    return fetch('https://zenquotes.io/api/random')
        .then(res => {
            return res.json(); 
        })
        .then(data => {
            // q = quote, a = author, zenquotes api의 문법
            return data[0]['q'] + ' -' + data[0]['a'];
        })
};

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`)
});

client.on('message', msg => {
    if (msg.author.bot) return; // 채팅한 사람이 봇일 경우 대답하지 않음

    if (msg.content === '$inspire') {
        getQuote().then(quote => msg.channel.send(quote))
    }

    db.get('responding').then(responding => {
        // responding의 value가 true일 경우만 반응해줌
        // sadWords안에 있는 곳을 살펴 본 후 msg에 word가 포함되어 있으면 실행
        if (responding && sadWords.some(word => msg.content.includes(word))) {
            db.get('encouragements').then(encouragements=> {
                const encouragement = encouragements[Math.floor(Math.random() * encouragements.length)];

                msg.reply(encouragement);
            });
        }
    })

    if (msg.content.startsWith('$new')) {
        encouragingMessage = msg.content.split('$new')[1]; //'$new'가 0번째 element가 됨
        updateEncouragements(encouragingMessage);
        
        msg.channel.send('New encouraging message added.');
    }

    if (msg.content.startsWith('$del')) {
        index = parseInt(msg.content.split('$del')[1]);
        deleteEncouragement(index);
        
        msg.channel.send('encouraging message deleted.');
    }

    if (msg.content.startsWith('$list')) {
        db.get('encouragements').then(encouragements => {
            msg.channel.send(encouragements);
        });
    }

    if (msg.content.startsWith('$responding')) {
        value = msg.content.split('$responding')[1];

        if (value.toLowerCase() == true) {
            db.set('responding', true);
            msg.channel.send('responding is on.')
        } else {
            db.set('responding', false);
            msg.channel.send('responding is off.')
        }
    }
});

client.login(TOKEN);

이제 $list$responding도 등록하여 정상적으로 작동할 것이다
남은건 내가 repl.it을 종료하여도 background상에서 이 봇이 계속 실행되도록 해주기만 하면 된다


첫번째는 왼쪽 상단에 보면 본인의 프로젝트 명이 보일텐데(내 경우는 Encourage Bot JS) 그걸 클릭하면 위와 같이 보일텐데 Upgrade to Hacker를 눌러 유료플랜을 사서 아래 Always On을 켜서 background에서도 활성화를 시키는 방법이다

두번째는 uptime robot을 사용하는 방법인데 일단 server.js라는 파일을 만들어 준 후

const express = require('express');

const server = express();

// '/'으로 지정하여 루트 도메인에서 모든 http 요청에 응답
server.all('/', (req, res) -> {
    res.send('Bot is running !')
});

function keepAlive() {
    server.listen(3000, () => {
        console.log('Server is ready.')
    });
}

module.exports = keepAlive;

위와 같이 작성해 준 후 다시 index.js(봇을 구현한 js file)의 상단에

const keepAlive = require('./server');

를 추가해 주고 클라이언트가 로그인 하기 전에 이 모듈을 실행시켜 준다

keepAlive();
client.login(TOKEN); //최하단에 있던 클라이언트 로그인 코드

위와 같이 작성한 후 Run을 누르면

봇이 실행중인 것을 확인할 수 있다

여기까지 정상적으로 따라왔다면 Uptime robot에 가입 후 로그인하고 왼쪽 상단의 Add New Monitor를 눌러서 아래와 같이 설정해주면 된다

참고로 URL은 본인의 repl.it에서 방금 server를 구현 후 Run을 눌렀을 때 나타났던 웹페이지의 URL을 입력하면 된다

Friendly Name을 바꾸고싶다면 본인이 원하는 것으로 설정하면 된다

이제 우리의 컴퓨터가 꺼져있고 repl.it을 꺼버려도 봇은 계속 실행되고 있을 것이다

여기까지가 내가 이 유튜브 강의를 보면서 배운 내용이고, 사실 나도 아직 javascript에 대한 지식이 부족하기 때문에 누군가의 궁금증을 해결해주기에는 턱없이 부족할 것이라고 생각한다

하지만 누군가는 이 게시물을 통해 무언가 창작을 하는 것에 있어 기쁨을 얻을 수 있으면 좋겠다






reference

I. Youtube: Code a Discord Bot with JavaScript - Host for Free in the Cloud
II. Discord.js
III. 자바스크립트 비동기 처리와 콜백 함수
IV. Array.prototype.some()
V. Map.prototype.get()
VI. Web-APIs: Fetch

좋은 웹페이지 즐겨찾기