글쓰기를 도와주는 디스코드 봇 만들기

김재연·2025년 2월 16일
10
post-thumbnail

링크

깃허브 : https://github.com/kpeel5839/manage-writing-discord-bot

왜 시작했을까?

글쓰기 시작

회사에 다닌지 한 6개월 쯤 다 되어가고 있는 것 같습니다. 배가 따뜻해지니, 예전보다 적극적으로 기술적인 부분에 대한 공부를 소홀히 하는 저를 발견할 수 있었습니다. 물론 한 순간도 공부를 완전히 놓은 적은 없고, 열심히 살고 있긴하지만, 현재보다 더 노력하려는 삶을 살기 위해, 글쓰기를 시작하려합니다.

어떤 일을 하기 위해서, 정말 효율적인 방법 중에 하나가, 같이 하는 사람을 만드는 것이라고 생각합니다. 그래서, 회사에서 마음이 맞는 사람들과 매주 기한을 정하고 글을 작성하는 작은 모임을 만들게 되었습니다.

이렇게 모임을 만들게되니, 사람이 해야하는 부분을 최대한 덜고 싶었습니다. 벌금, 인증, 기한 정하기 등등 말이죠.

그래서 이를 해결하기 위해 디스코드 봇을 만들기로 계획하고 실행하게 되었습니다.

또, 굳이 굳이 디스코드 봇을 제가 직접 만든 이유가 있는데요. 그 이유는 바로 다음에 있습니다.

서버만 있어도 된다.

저는 현재 회사에서도, 그리고 지금까지 개발을 접해오고 해온 과정도 모두, 클라이언트 개발자들과 함께해왔습니다.
보통 서버에서 개발한 기능을 사용자들에게 보여주기 위해서는 이들이 꼭 필요했죠. (물론 이 협업 과정이 너무 재미있기도 하고, 제가 좋아하는 부분이긴 합니다.)

그렇기 때문에, 어떠한 기능을 만들고 싶다거나, 조금 더 현재로부터 개선하고 싶다라고 했을 때, 클라이언트와의 의견 조율 과정이 많았습니다.

하지만, 디스코드 봇을 만들 때에는 이런 부분이 거의 사라집니다. 디스코드라는 이미 사용자들과 소통할 수 있는 매개체가 존재하기 때문에, 이전에 고려해야 했던 부분들을 고려하지 않고, 제가 생각한 것들을 마음껏 펼칠 수 있습니다.

즉, 요약하면 저 혼자서도 재미있게 제 생각을 펼칠 수 있는 프로젝트를 만든거죠.

기획

글을 쓸 때, 관리 포인트가 어느 것이 있는지 고민해보았습니다.

그래서, 조금 생각해보았을 때, 목표 정하기, 인증하기, 벌금 고지하기 등이 있더군요.

그래서 다음 기능들을 어느 정도 자동화 해 봇이 관리할 수 있도록 다음과 같이 기획 했습니다.

목표 정하기

목표를 정할 때 필요한 요소는 기한 날짜, 글 개수, 할당된 인원 정도가 있을 것 같습니다.

그래서, 다음과 같은 메시지 Format을 지켜 디스코드에 채팅을 입력하면, 봇이 이를 인지해 스레드를 생성할 수 있게 해줄 것입니다.

인증하기

위와 같이 목표를 정하고 나면, 스레드가 생성될 것입니다.

그렇게 생성된 스레드에 !인증 (글 링크) 라고 입력하면 인증이 되도록 합니다.

추후에는 자동적으로 인증이 되는 구조를 만들어, 조금 더 신뢰성 높고, 사용성 좋게 발전시켜 나갈 예정입니다. ex) rss?

벌금 고지하기

기한을 넘어가면 벌금을 고지합니다.

이 때는 글을 몇 개를 못 썼는지와, 못 쓴 글 1개당 벌금의 가격을 활용하여 벌금을 계산합니다.

그리고 위와 같이 멘션을 통해 목표를 이행하지 못한 자에게 알림을 보냅니다.

서버는 어떻게?

해당 디스코드 봇을 띄우기 위해 유료 서비스(aws, naver cloud, etc)를 사용하고 싶지는 않았습니다.

그래서, 가볍게 github action을 통해 디스코드 봇을 계속 가동한 상태로 유지합니다. (Public Repository의 경우 무제한 무료임을 확인하고 진행했습니다.)

구현

전체적인 느낌

일단 컨셉 자체를 다음과 같이 잡고 진행했습니다.

  • Discord 라이브러리 사용
  • Discord의 메시지 목록들을 하나의 DB로 보고 개발을 진행했습니다.
  • 디스코드 봇이 재 가동될 때, Down Time이 잠깐 존재하기 때문에, Application이 뜰 때, 모든 메시지를 읽어 혹여나 처리하지 못한 메시지에 대한 처리를 진행했습니다.
  • 최대한 도메인 주도적으로 개발
  • 가장 익숙한 3-layered-architecture 구조

목표 정하기

일단, 목표가 가장 큰 도메인 덩어리입니다. (현재로써는 글쓰기의 가장 근간이 되는 부분)

  • 전체적인 도메인 구조


다음과 같은 구조를 가지고 있다고 생각하시면 됩니다.

그래서 WritingAuthorization을 만들 때 각각의 요소들을 만들면서 유효성 검증과, 만듦과 동시에 Thread를 생성하고 목표를 고지하는 역할을 합니다.

도메인 WritingAuthorization 코드

가장 사이즈가 큰 도메인이라서 코드가 너무 길어 링크로 남기겠습니다.

인증

인증은 목표를 정할 때, 할당되었던 유저가 !인증 링크 를 입력하면 동작합니다.

스레드에 입력된 메시지를 AuthorizationMessage로 만들면서 유효성 검증을 해주고 다음과 같은 요소들로 나누어줍니다.

async def authorize_link_by_message( # in WritingAuthorization
    self,  
    authorization_message: AuthorizationMessage,  
    writing_goal: int = 0,  
    send_message: bool = False  
):  
  message = authorization_message.message  
  for assignee in self.assignees:  
    if not assignee.is_same_id(message.author.id):  
      continue  
  
    is_successful_add = assignee.authorize_link(authorization_message.link)  
  
    if not send_message: # 스레드에 메시지를 보낼 것인지 결정
      return  
  
    if not is_successful_add: # 인증 정보가 유효하지 않은 경우
      await message.reply(  
          self.FAILED_AUTHORIZATION_MESSAGE.format(assignee.assignee.name)  
      )  
      return  
  
    await message.reply(self.SUCCESS_AUTHORIZATION_MESSAGE.format(  
        assignee.assignee.name,  
        assignee.written_link(),  
        len(assignee.links),  
        max(0, writing_goal - len(assignee.links))  
    ))

def authorize_link(self, add_link: URL): # in Assignee
  if add_link in self.links or not add_link.is_valid():  
    return False  
  
  self.links.append(add_link)  
  return True

그리고 위와 같은 코드로, Authorization Message를 입력받고 조건에 맞는 assignee에게 이를 할당하고, 메시지를 발송해줍니다.

  • 실패

  • 성공

벌금 고지

매일 00시에 스케줄링을 통해, 벌금을 때릴 것인지 말 것인지 확인을 합니다.

async def mention_penalty_to_user(self): # in WritingAuthorization
  message_thread = self.original_message.thread  
  now_time_in_seoul = (datetime.datetime.now(ZoneInfo("Asia/Seoul"))  
                       .replace(tzinfo=None))  
  if not self.is_valid_message() or message_thread is None or self.date_decision.date.date() >= now_time_in_seoul.date(): # 기한이 지났는지 확인
    return  
  
  for assignee in self.assignees.assignees:  
    penalty_prefix = self.PENALTY_MESSAGE_PREFIX.format(assignee.assignee.id) 
    remain_writing = assignee.lack_of_writing(self.post_limit_decision.limit)  
    if self.authorization_thread.already_exists_content_with_prefix( # 메시지를 이미 보낸적이 있는지 확인, 앱이 시작할 때 모든 메시지를 처리하기 때문에
        penalty_prefix  
    ) or remain_writing == 0:  
      continue  
  
    penalty_message = self.PENALTY_MESSAGE.format(
        penalty_prefix,  
        self.PENALTY_COST,  
        remain_writing,  
        remain_writing * self.PENALTY_COST  
    )  
    await message_thread.send(penalty_message) # 페널티 메시지 생성 및 전송

def lack_of_writing(self, limit: int): # in Assignee
  return max(0, limit - len(self.links)) # 목표에서 얼마나 부족한지 계산, 최소 0

위와 같이 로직을 처리해, 다음과 같이 멘션을 날리게 됩니다.

계속 Github Action 돌리기

계속해서 Github Action을 돌리는 것은 다음과 같이 Workflow를 작성했습니다.

name: Run Discord Bot  
  
on:  
  schedule:  
    - cron: "0 */3 * * *"  # 3시간마다 실행  
  workflow_dispatch:  # 수동 실행 가능  
  
concurrency:  
  group: discord-bot  
  cancel-in-progress: true  # 이전 실행이 있으면 강제 종료  
  
jobs:  
  run-bot:  
    runs-on: ubuntu-latest  
  
    steps:  
      - name: Checkout repository  
        uses: actions/checkout@v4  
  
      - name: Set up Python  
        uses: actions/setup-python@v4  
        with:  
          python-version: "3.x"  
  
      - name: Install dependencies  
        run: |  
          python -m pip install --upgrade pip  
          pip install discord.py apscheduler aiohttp aiosignal pytz schedule tzlocal audioop-lts  
  
      - name: Run script  
        env:  
          DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}  
          AUTHORIZATION_CHANNEL_ID: ${{ secrets.AUTHORIZATION_CHANNEL_ID }}  
        run: python presentation/Main.py

다음과 같이, 3시간 씩 돌아갈 수 있게, 또한, Action이 실행되면 이전 Action이 종료될 수 있게 작성해주었습니다. (나머지는 의존성이랑 스크립트 실행시키기)

이를 통해, 봇이 잠깐 잠깐 내려가는 시간은 존재하지만, 거의 항상 떠있는 구조를 만들 수 있었습니다.

결과

목표 정하기

목표가 잘 설정되는 것을 볼 수 있습니다.

인증

인증도 다음과 같이 잘됩니다.

이미 인증한 URL과 같은 경우 다음과 같이 예외처리가 되는 것을 볼 수 있습니다.

벌금 고지


또한, 벌금 고지도 정상적으로 잘 이루어지는 것을 볼 수 있습니다.

추후 방향

약간 티켓 느낌으로 Issue에 앞으로 생겼으면 하는 기능들을 작성하고 있습니다.

이들을 저 혹은 같이 글쓰기 모임(어짜피 전부 개발자)을 하는 분들과 함께 시간이 될 때, 개발을 순차적으로 진행할 예정입니다. 최대한 좋은 기능들을 많이 넣는 것이 제 목표입니다. (그래서 이번에는 본격적으로 레이어도 나누고, 도메인도 관리하며 개발한 것입니다. 이전에 만들었던 봇들은 다음과 같습니다. 🔗사이드 프로젝트 스크럼 봇, 🔗회사 회식 설문조사 봇)

긴 글 읽어주셔서 정말 감사합니다.

profile
끊임없이 '성장'하는 개발자 김재연입니다.

6개의 댓글

comment-user-thumbnail
2025년 2월 16일

생각해보니 봇은 항상 가동해야 하는 개념이며, 이 봇을 구동시키기 위해서 github action을 사용하셨었군요. 주제에 걸맞는 재미있는 포스팅이었어요. Good job 🦔👍

1개의 답글
comment-user-thumbnail
2025년 2월 17일

잘 읽었어요 :) 일상에서 불편함을 느낀 부분을 바로 해결하려고 하는 부분이 정말 찐개발자 느낌이 나네요.

모임을 운영하다보면 단순한 넛지로는 동기부여를 꾸준하게 유지시키기 힘든 시점이 오는데, 이런 부분에 대한 고민도 녹아있으면 정말 재밌는 모임이 될 것 같아요 (참고로 저는 Gamification 에 주목하고 있어요).

1개의 답글
comment-user-thumbnail
2025년 3월 5일

나 WeSpot 디자이너인데, 김재연이 만든 스크럼봇 덕분에 데일리 스크럼 편하게 할 수 있었다

1개의 답글

관련 채용 정보