Rust로 만든 디스코드 봇

Pt J·2020년 12월 13일
0

Story Of My Life

목록 보기
18/31
post-thumbnail

구현 환경

봇 생성

먼저 디스코드 개발자 포털에서 디스코드 어플리케이션을 만들어야 한다.
링크를 통해 접속해서 New Application 버튼을 통해 새 어플리케이션을 생성한다.

어플리케이션의 이름을 작성하고 어플리케이션을 생성하면
General Information으로 접속되는데
여기서 봇의 이름과 설명, 사진 등을 설정하고
왼쪽 상단의 \equiv 버튼을 눌러 Bot으로 이동한다.
Add Bot 버튼을 눌러 봇을 생성한다.
이 때, 설정한 이름을 가진 사용자가 너무 많다며 거부될 수도 있다.

Too many users have this username, please try another. (username)

이 경우 General Information으로 돌아가 적절한 이름으로 변경하여 재시도하자.
봇을 생성하면 General Information에서 설정한 이름과 사진이 적용되어 있다.

봇을 추가했다면 다시 \equiv 버튼을 눌러 OAuth2로 이동한다.
SCOPES에서는 bot을 선택하고
BOT PERMISSIONS에서는 Send MessageRead Message History를 선택한다.
혹은 자신이 만들고 싶은 봇에 추가적으로 필요한 권한이 있다면 더 선택한다.

다 선택했으면 위로 올라가 SCOPESCopy 버튼을 눌러 OAuth URL을 생성한다.
이것을 새 브라우저에 붙여넣고 디스코드 계정으로 로그인하여
관리 권한을 가진 서버를 선택하고 Continue, Authorize를 눌러 봇을 초대한다.

마지막으로 디스코드 개발자 포털로 돌아가 다시 Bot으로 이동한다.
봇의 USERNAME 아래 보이는 TOKENCopy를 눌러 토큰을 얻어온다.
이 토큰은 잊어버리지 않게 어딘가에 잘 기록해두도록 하자.
단, 이것은 봇을 관리하는 데 사용되므로 유출되지 않도록 주의하자.

봇 구현

@피터의 주언어는 Rust이므로 Rust를 이용해 봇을 작성할 것이다.
Rust를 위한 환경 구축이 되어 있다고 가정한다.

cargo

@피터가 만들 디스코드 봇 이름은 Jason 이므로
프로젝트 이름은 jason_bot으로 설정하도록 하겠다.

$ cargo new jason_bot
     Created binary (application) `jason_bot` package
$ cd jason_bot
jason_bot$

Cargo.toml을 열어 의존 패키지를 추가한다.
TokioSerenity를 추가해주면 된다.

jason_bot$ vi Cargo.toml

Cargo.toml

[package]
name = "jason_bot"
version = "0.1.0"
authors = ["Pt J <peter.j@kakao.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serenity = { version = "0.9", features = ["client", "gateway", "rustls_backend", "model", "framework", "standard_framework"] }
tokio = { version = "0.2", features = ["macros"] }

여기서 Tokio는 비동기 프로그램 작성을 위한 크레이트고,
Serenity는 Discord API를 사용하기 위한 크레이트다.
Serenity에 대한 예제는 Serenity Github 저장소에서 확인할 수 있다.
그 외 추가적으로 필요한 게 있다면 그 때 가서 또 추가하면 충분하다.

@피터의 디스코드 봇 의존성은 최종적으로는 다음과 같았다.

[dependencies]
serenity = { version = "0.9", features = ["client", "gateway", "rustls_backend", "model", "framework", "standard_framework"] }
tokio = { version = "0.2", features = ["macros"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

정확히는 다음과 같은 것들을 src/main.rs의 범위에 가져와 사용하였다.

src/main.rs

// fr Environment Variable
use std::{collections::{HashMap, HashSet}, env, sync::Arc};

// for Discord API
use serenity::{
    async_trait,
    client::bridge::gateway::ShardManager,
    framework::standard::{
        Args, CommandOptions, CommandResult, CommandGroup, DispatchError, HelpOptions, help_commands, Reason, StandardFramework, macros::{
            command, group, help, check, hook,
        },
    },
    http::Http,
    model::{
        channel::{
            Message, ReactionType::Unicode,
        },
        gateway::Ready, id::{
            UserId, ChannelId,
        },
    },
    utils::Colour,
    prelude::*,
};

// for JSON parsing
use serde::{Deserialize, Serialize};

features

지극히 개인적인 봇으로, 우리 서버에서만 가동하고 따로 배포할 생각이 없기에
모든 걸 다 내 마음대로 구성하였다.

사용할 수 있는 명령어는 help about ping send say react
help는 명령어를 설명해주는 녀석인데
대부분의 명령어에는 description이 적혀 있지 않다는 게 함정.
...이건 추가 작성하는 게 좋을 것 같긴 하다.
about은 봇에 대한 설명 명령어지만 그냥 간단히 한 줄 소개로 끝냈다.
ping은 기본적인 ping-pong 예제의 흔적.
그리고 나머지 세 개가 메인 기능들이다.

send

send는 어떤 채널에 embed message를 보내는 명령어다.
채널을 설정하지 않고 기본값으로 하겠다고 하면 명령어를 입력한 채널에 보내진다.
embed message가 기본 목적이기 때문에 일반 텍스트는 선택 사항이다.

예를 들어 보낼 내요은 다음과 같은 JSON data로 설정하는데

{
        "content" : "the JSON data for the test",
        "title" : "The Test",
        "description": "This is a test data for embedding",
        "colour": 14501908,
        "fields": [
                ["title1", "content1", true], ["title2", "content2", true], ["title3", "content3", false]
        ],
        "bind": "default"
}

다음과 같이 content를 empty string으로 두면 일반 텍스트를 생략할 수 있다.

{
        "content" : "",
        "title" : "The Test",
        "description": "This is a test data for embedding",
        "colour": 14501908,
        "fields": [
                ["title1", "content1", true], ["title2", "content2", true], ["title3", "content3", false]
        ],
        "bind": "default"
}

say

say는 어떤 채널에 일반 텍스트를 보내는 명령어다.
send로는 일반 텍스트는 생략할 수 있지만 embed message는 생략할 수 없어
단순 일반 텍스트 전달용으로 추가 구현하였으며,
embed data가 빠졌다는 걸 제외하면 send와 동일하다.
여기선 content를 생략하면 아무런 동작도 하지 않는다.

JSON argument는 다음과 같이 전달할 수 있다.

{
        "content" : "the JSON data for the test",
        "bind": "default"
}

react

react는 어떤 메시지에 이모지로 반응을 하는 명령어다.
사실 처음엔 메시지를 지정하지 않고 이모지만 전달하여
가장 최근에 보낸 메시지에 이모지를 설정하도록 할 생각이었는데
삽질을 좀 하다가 현재의, 채널 ID와 메시지 ID를 요구하는 형태가 되었다.
반응 생성용으로 제공되는 create_reaction 함수를
전달된 NN개의 이모지에 대하여 일괄 적용하는 정도?

JSON argument는 다음과 같이 전달할 수 있다.

{
        "c_id" : 773540505266421812,
        "m_id" : 787729895257931837,
        "reactions" : [":new_moon:", ":last_quarter_moon:", ":full_moon:", ":boom:"]
}

시연

우리 디스코드 서버에 오너 @하찌@피터만 접근할 수 있는 명령어용 채널을 만들어
다른 채널에 메시지를 전달하도록 하였다.

say

react 함수가 구현되기 전에 일단 봇부터 초대한 흔적이다.

send

이 서버엔 유용해보이는 IT행사를 소개해주는 채널 #ㄱㄱㄱㄱㄱ이 있는데
보통 @피터가 작성하지만 이번엔 @제이슨이 작성하도록 하였다.
이미 소개된 행사지만 마침 오늘 진행되는 기술 컨퍼런스가 있길래 D-DAY 알림으로...ㅎ

react

이 서버엔 일부 멤버에게 공부를 시키는 채널도 존재하는데
이건 혼자서는 공부를 너무 안한다며 강제성을 부여해달라는 친구들이 있어 만들어진 것이다.
얘들아 공부하자^^라는 이름의 카테고리에
github의 공부용 저장소를 webhook로 연동한 #공부-깃허브 채널,
숙제를 올려주며 이를 확인하고 관련 이야기를 하는 #공부-인증 채널,
공부하다 생긴 질문에 대한 답변을 위한 #피터-이거-답변-좀 채널,
그리고 음성 채널 채팅으로 전달되지 않는 것이 존재한다.
보통 숙제는 스케줄 봇 sesh를 통해 투표 기능으로 진행을 확인했는데
필요로 하는 것 이상의 기능을 가지고 있어 오히려 적절치 않은 면이 있었다.

따라서 @제이슨을 통해 이를 대체하였다.
그리고 #공부-인증 채널에서 고정된 메시지로 설정해놓는 기존의 방식은
숙제와 유관한 잡담 속에 숙제 정보가 묻히게 되고
고정 메시지 확인 버튼을 눌러야만 확인이 가능하다는 점에서
숙제용 채널 #숙제-시키는 곳을 추가하여 여기에 메시지를 보냈다.
이 채널은 @피터와 서버 오너 @하찌, 그리고 봇만 메시지 전송이 가능하며
숙제를 하는 친구들은 단지 메시지 기록을 보고 이모지로 반응하는 것만 가능하다.

후기

사실 sesh에 대하여, 다른 스케줄 기능은 굳이 필요하지 않고
투표에도 선택지를 추가하는 등의 기능은 필요하지 않다고 느끼던 중
"이 정도 챗봇은 나도 직접 구현할 수 있지 않을까?" 하는 생각이 든 것은
한창 오픈북이 아닌(...) 기말고사 시험을 앞둔, 시험 몇 시간 전이었으며
그 땐 Rust가 아니라 Python으로 구현할 생각이었다.

그런데 주언어가 Rust라고 하면서도 막상 Rust로 뭘 해본 게 없는 것 같고
Python은 이런 때 아니더라도 언제라도 쓰게 될테니
Rust로 구현하면 좋겠다고 방향을 틀었다.
// 다행히도 pip install discord까지만 한, 매우 초반부에 방향을 틀었다.
// 사실 의미부여일 뿐, Python으로 구현을 시도하다가 매우 초반부 포기(...)하고 방향을 튼 거에 가깝지만...

금요일 오전에 기획을 시작하였는데 약 사흘만에 구현을 완료하였다.
그리고 우리 디스코드 서버에서 유용하게 사용할 예정이다.
@피터는 구현 완료 후 시연 몇 번 해보고 끝나는 프로젝트보다
이렇게 실제로 유용하게 사용할 수 있는 프로젝트를 선호한다.

생각보다 삽질을 참 많이 하긴 했다.
example이 존재하는 함수에 대해서는 쉽게 사용할 수 있었지만
그렇지 않은 부분에서 좀 헤맸다.
input argument가 JSON format인 건... 단지 이게 parsing하기 편해서...ㅎㅎ
일반 텍스트로 argument를 받아서 처리할 수도 있긴 하지만
@피터는 전부 JSON format으로 받도록 구현했다.

~!가 아닌, 멘션을 prefix로 삼은 것은 sesh에게서 아이디어를 얻었다.
// 그래놓고 sesh를 내친다... 토사구팽인가

@제이슨의 코드는 이름에 링크되어 있는 @피터의 github 저장소에서 확인할 수 있다.

일상으로 돌아가 내 할 일 하다가
조만간 help description이랑 edit 기능 추가해야짘

Reference

profile
Peter J Online Space - since July 2020

0개의 댓글