블로깅 챌린지 운영기 : 디스코드 챗봇 v1.0 만들기

길하균(Lagun)·2023년 9월 20일

이전 : 블로깅 챌린지 디스코드 챗봇 구상하기

이전 포스팅에서 블로깅 챌린지 목적와 디스코드 챗봇을 만들게 된 이유에 대해서 간단히 소개했습니다. 그리고 디스코드 챗봇을 만들기 위한 구상안도 같이 적었는데요. 궁금하신 분들은 아래 링크를 방문해주시면 감사하겠습니다.

🔥 블로깅 챌린지 운영기 : 디스코드 챗봇 구상하기

이번 글에서는 제가 만든 디스코드 챗봇의 코드들을 설명하려고 합니다. 챗봇을 구동하는데 필요한 파일이 2개여서 파일별, 기능별로 나눠서 정리할 예정입니다. 그리고 마지막에는 전체 코드를 올렸으니 전체 흐름이 궁금하신 분은 바로 최하단으로 내려가시면 될 것 같습니다.

제가 코딩 실력이 뛰어난 편은 아니라서 블로그에 코드를 올리는 것이 부끄럽지만 기록 차원에서 적어보려고 합니다. 혹시 피드백을 해주실 분은 이 글의 댓글이나 ghaguniv@gmail.com으로 이메일 남겨주시면 감사하겠습니다 :)

Part IV : keep_alive

keep_alive 파일의 역할은 챗봇이 24시간 작동할 수 있도록 하는 것입니다. 이 파일에서는 uptimerobot이 접속할 수 있는 주소를 만듭니다. 그리고 나중에 uptimerobot에 만들어진 url을 등록해두면 uptimerobot이 이 파일을 사용하는 코드를 계속 작동하게 하면서 챗봇이 반응하도록 할 수 있습니다. 제가 구상한 방법은 아니고 구글에 '디스코드 챗봇 24시간'이란 키워드로 검색해서 나온 방법입니다.

from flask import Flask
from threading import Thread
 
app = Flask('')
 
@app.route('/')
def home():
    return "Bot is online"
 
def run():
    app.run(host='0.0.0.0',port=8080)
 
def keep_alive():
    t = Thread(target=run)
    t.start()

코드에 대해 간략히 설명해보자면 home 함수는 생성된 url에 띄울 텍스트를 의미합니다. 이 코드를 통해 만들어진 url에 들어가면 "Bot is online"이라는 문구가 떠 있습니다. run()함수는 host, port 값을 설정합니다. 마지막으로 keep_alive()함수는 위에서 설정한 값을 토대로 홈페이지를 작동하게 만들어줍니다.

Part V : main

main 파일이 디스코드 챗봇의 코드가 들어갈 파일입니다. 아직 파일을 분리해서 관리하는 방식이 익숙하지 않아서 챗봇의 모든 코드가 여기에 들어있습니다. 그래서 최대한 기능별로 나눠서 설명하려고 하니 참고해주세요!

import discord
import numpy as np
from replit import db

from keep_alive import keep_alive

keep_alive()

TOKEN = 'discord token'

우선 필요한 라이브러리를 import하고 앞에서 만들었던 keep_alive함수를 호출합니다. 여기서 replit의 db는 제가 코드를 작성한 사이트에서 제공하는 data base를 사용하기 위한 라이브러리입니다. replit에서는 간단한게 db를 생성하고 사용할 수 있는 라이브러리를 제공하고 있었습니다. 챗봇이 중간에 재부팅 되는 경우가 있기 때문에 챌린지 성적을 기록하고 유지하기 위해서는 db를 이용해야 했습니다. 라이브러리 호출 후에는 디스코드에서 발급 받은 토큰을 TOKEN 변수에 넣어둡니다.

class MyClient(discord.Client):
  # 리스트 형태로 점수 관리. [블로그 등록, 게시물, 회고록, 정보글, 반응]
  score_list = np.array([10, 15, 30, 5, 1])

  async def on_ready(self):
    print('Logged on as {0}!'.format(self.user))
    await self.change_presence(status=discord.Status.online,
                               activity=discord.Game("🔥🔥🔥"))

다음은 챗봇의 클래스를 설정하는 코드입니다. 저는 최상단에 점수 계산에 사용할 numpy array를 만들었습니다. 나중에 활동 기록 array랑 곱연산을 해서 점수를 구할 예정입니다. on_ready()는 discord 라이브러리에서 정의되어 있는 함수입니다. 이 함수는 챗봇이 구동되는 이벤트를 트리거로 작동합니다. 위 코드는 챗봇이 구동되었을 때 print문을 출력하고 디스코드에 🔥🔥🔥하는 중이라는 상태메시지를 띄우게 됩니다.

이후에 살펴볼 코드들은 모두 class MyClient 내에 들어가는 코드입니다. 한번에 클래스 코드를 다 쓰기에는 글이 길어져 읽기 불편할 것 같아서 부득이하게 나눠서 쓰려고 합니다.

async def on_message(self, message):
    id = message.author.id
    name = message.author.global_name
    content = message.content

    if '!명령어' in content:
      await message.channel.send(
          '!뒤에 원하시는 명령어를 입력해주세요. \n 사용 가능한 명령어 : 등록, 게시물, 회고록, 정보글, 점수, 기록')

    if '!등록' in content:
      if (id not in db.keys()) & ('https' in content):
        db[str(id)] = [1, 0, 0, 0, 0]
        db["name_info"][id] = name
        await message.channel.send(f'환영합니다!🤗 \n{name}님의 id는 {id}입니다.')
      elif 'https' not in content:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

on_message()함수는 디스코드에 메시지를 보냈을 때 작동하는 코드입니다. 이 함수가 작동하면 message 안에 지정한 명령어가 있는지를 확인하고 각 명령어를 수행하도록 만들었습니다.

우선 '!명령어'라는 글자가 메시지에 들어있다면 챗봇이 할 수 있는 작업들에 대해서 출력합니다. v1.0 기준으로는 등록, 게시물, 회고록, 정보글, 점수, 기록 등의 작업을 할 수 있습니다.

'!등록'이라는 명령어는 우선 url이 포함되어 있는지 그리고 이미 등록을 한 유저가 아닌지를 확인합니다. 이 두 조건을 만족한다면 id를 키값으로 가지는 db를 하나 만들고 첫 성적을 기입합니다. 그리고 'name_info'라는 db에 아이디와 이름을 넣어줍니다. 그리고 환영인사를 디스코드 채팅방에 보냅니다.

만약 위 조건을 만족하지 못한다면 url 주소가 없기 때문일 확률이 높기 때문에 명령어 뒤에 url 주소를 같이 기입하라는 안내문구를 보내게 됩니다. 이미 등록되어 있는 경우는 발생할 일이 없을 것 같아 따로 처리하지 않았습니다.

if '!게시물' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][1] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📄 게시물 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

    if '!회고록' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][2] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📅 회고록 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

    if '!정보글' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][3] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📑 정보글 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 URL 주소를 같이 보내주세요.')

위 코드들의 작동 방식은 동일하기 때문에 한번에 설명하려고 합니다. 우선 각 명령어가 message에 포함되어 있다면 등록을 한 유저인지를 먼저 확인합니다. 만약 아직 등록을 하지 않았다면 등록을 먼저 하라는 안내 메시지를 보내게 됩니다.

등록이 되어 있는 유저의 경우 활동횟수 1을 추가하고 점수 array와 활동 횟수 array를 곱연산 시켜서 score 값을 구합니다. 그리고 메시지로 어떤 활동을 했는지, 점수는 몇점인지를 알려줍니다.

같은 방식의 코드를 왜 반복해서 사용하는지, 그냥 함수를 하나 만들어서 쓰면 되는거 아닌가라는 생각이 드실 수도 있습니다. 이때는 아직 class 내의 변수들을 이용하는데 익숙하지 않아서 케이스별로 다 따로 코딩했었습니다. 당장 서비스를 해야하는 상황이라 기능 구현이 더 급하기도 했었습니다.. 물론 이후에 시간을 가지고 수정해놨습니다ㅎ 다음 게시물에서 어떻게 바뀌었는지 봐주시면 감사하겠습니다!

if '!점수' in content:
      print(db[str(id)])
      score = np.sum(self.score_list * np.array(db[str(id)]))
      await message.channel.send(f'{name}님의 점수는 {score}입니다.')

    if '!기록' in content:
      history = db[str(id)]
      await message.channel.send(
          f'{name}님의 현재까지 기록은 \n 블로그 등록 : {history[0]}회 \n 게시물 공유 : {history[1]}회 \n 회고록 공유 : {history[2]}회 \n 정보글 공유 : {history[3]}회 입니다.'
      )

다음은 유저들이 자신의 점수와 현재까지의 기록을 확인할 수 있는 코드입니다. 메시지에 '!점수'가 포함되어 있다면 메시지를 보낸 유저의 id를 이용해서 현재까지의 기록을 찾습니다. 그 다음 array간 곱연산으로 점수를 구하게 됩니다.

'!기록' 명령어가 메시지에 포함되어 있다면 우선 현재까지의 활동기록을 찾아옵니다. 그리고 각 활동별로 몇 회를 했는지 알 수 있도록 f문자열 방식으로 메시지를 보내게 됩니다.

if '!관리' in content:
      for key in db["name_info"].keys():
        await message.channel.send(f'{key} / {db["name_info"][key]}')

        if key in db.keys():
          await message.channel.send(f'key : {key}, value : {db[key]}')
        else:
          await message.channel.send('존재하지 않는 id입니다.')

    if '!수정' in content:
      com, id, sco1, sco2, sco3, sco4, sco5 = content.split(" ")
      db[str(id)] = [int(sco1), int(sco2), int(sco3), int(sco4), int(sco5)]

    if '!이름수정' in content:
      com, name, id = content.split(" ")
      db["name_info"][id] = name

다음은 유저 DB 관리를 위한 코드입니다. 현재까지의 활동 횟수를 확인하고 데이터를 수정할 수 있는 기능을 구현하였습니다. 메시지에 '!관리'가 포함되어 있다면 for문에서 key 변수에 name_info의 key값을 차례대로 넣습니다. 그 다음 해당 id와 name을 디스코드 메시지로 보내게 됩니다. 그 다음 해당 key값이 활동횟수 db에 들어있는지를 확인합니다. 만약 들어있다면 활동횟수 array를 출력하고 그렇지 않으면 존재하지 않는 id라는 메시지를 보내게 됩니다.

'!수정'이라는 단어가 메시지에 포함되어 있다면 챗봇은 명렁어 뒤에 있는 id와 숫자들을 이용해서 기존 데이터를 수정합니다. !수정 명렁어 뒤에는 id, 각 활동의 횟수가 스페이스 한 칸씩 떨어져서 들어와야 합니다.

'!이름수정'이라는 명령어가 메시지에 포함되어 있다면 함께 들어온 id를 이용해서 name_info DB에서 이름을 수정하게 됩니다.

다음 : 챗봇 v2.0 설명

디스코드 챗봇을 만들어본 경험이 있으시거나 코딩이 익숙해진 분이라면 이번 포스팅에서 수정하고 싶은 부분들이 많으실거라 생각합니다. 저 역시 챗봇 코드를 전체적으로 새로 만들고 싶은 욕구가 있었습니다. 그래서 DB 형식부터 시작해서 챗봇의 많은 부분들을 수정했습니다. 처음부터 제대로 기획해서 한번만 일할걸 왜 두번이나 이런 복잡한 일을 했는지 모를 정도로 많은 부분을 개선했습니다. 그래서 다음 포스팅에서는 챗봇 v2.0을 주제로 글을 써보려고 합니다.

새롭게 추가된 기능도 있고 DB 형식도 바뀌다보니 코드가 많이 길어졌습니다. 그래서 아마 2~3개로 나눠서 포스팅하게 될 것 같네요.

전체 챗봇 코드

# keep_alive.py
from flask import Flask
from threading import Thread
 
app = Flask('')
 
@app.route('/')
def home():
    return "Bot is online"
 
def run():
    app.run(host='0.0.0.0',port=8080)
 
def keep_alive():
    t = Thread(target=run)
    t.start()
    
#main.py
import discord
import numpy as np
from replit import db

from keep_alive import keep_alive

TOKEN = 'discord token'


class MyClient(discord.Client):
  # 리스트 형태로 관리. [블로그 등록, 게시물, 회고록, 정보글, 반응]
  score_list = np.array([10, 15, 30, 5, 1])  # 리스트 형태로 점수 관리.

  async def on_ready(self):
    print('Logged on as {0}!'.format(self.user))
    await self.change_presence(status=discord.Status.online,
                               activity=discord.Game("🔥🔥🔥"))

  async def on_message(self, message):
    id = message.author.id
    name = message.author.global_name
    content = message.content

    if '!명령어' in content:
      await message.channel.send(
          '!뒤에 원하시는 명령어를 입력해주세요. \n 사용 가능한 명령어 : 등록, 게시물, 회고록, 정보글, 점수, 기록')

    if '!등록' in content:
      if (id not in db.keys()) & ('https' in content):
        db[str(id)] = [1, 0, 0, 0, 0]
        db["name_info"][id] = name
        await message.channel.send(f'환영합니다!🤗 \n{name}님의 id는 {id}입니다.')
      elif 'https' not in content:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

    if '!게시물' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][1] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📄 게시물 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

    if '!회고록' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][2] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📅 회고록 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 블로그 주소를 같이 보내주세요.')

    if '!정보글' in content:
      if db[str(id)][0] != 1:
        await message.channel.send('📢 등록을 먼저 진행해주세요!')

      elif 'https' in content:
        db[str(id)][3] += 1
        score = np.sum(self.score_list * np.array(db[str(id)]))
        await message.channel.send(f'📑 정보글 공유! \n{name}님의 현재 점수는 {score}점 입니다.'
                                   )
      else:
        await message.channel.send('✅ 명령어 뒤에 URL 주소를 같이 보내주세요.')

    if '!점수' in content:
      print(db[str(id)])
      score = np.sum(self.score_list * np.array(db[str(id)]))
      await message.channel.send(f'{name}님의 점수는 {score}입니다.')

    if '!기록' in content:
      history = db[str(id)]
      await message.channel.send(
          f'{name}님의 현재까지 기록은 \n 블로그 등록 : {history[0]}회 \n 게시물 공유 : {history[1]}회 \n 회고록 공유 : {history[2]}회 \n 정보글 공유 : {history[3]}회 입니다.'
      )

    if '!관리' in content:
      for key in db["name_info"].keys():
        await message.channel.send(f'{key} / {db["name_info"][key]}')

        if key in db.keys():
          await message.channel.send(f'key : {key}, value : {db[key]}')
        else:
          await message.channel.send('존재하지 않는 id입니다.')

    if '!수정' in content:
      com, id, sco1, sco2, sco3, sco4, sco5 = content.split(" ")
      db[str(id)] = [int(sco1), int(sco2), int(sco3), int(sco4), int(sco5)]

    if '!이름수정' in content:
      com, name, id = content.split(" ")
      db["name_info"][id] = name


intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
client.run(TOKEN)
profile
AI, 빅데이터를 배우기 위해 항해 중

0개의 댓글