글
← 전체 글CTF / CTF 디스코드 구축

초기 세팅

디스코드 봇

디스코드를 보면 다음과 같이 CTF면 티켓을 처리할 수 있는 앱이나 노래를 틀어주는 앱 같은 것들을 볼 수 있음
그 외에도 많은 앱들이 마켓 플레이스에 존재함

디스코드 봇은 부여받은 권한 안에서 서버를 관리하고, 명령어 실행, 메시지 전송, 외부 서비스 연동 등의 기능을 수행할 수 있음

이러한 기능들은 디스코드에서 공식적으로 제공하는 API 와 상호작용 기능을 활용하여 구현할 수 있음

image

디스코드 봇 만드는 방법

봇 생성

디스코드 봇은 Discord Developer Portal 에서 생성이 가능하고 python 이나 JS 로 기능 개발 후 디스코드 서버에 초대해서 사용하는 방식임

나는 CTF 할 때 자동으로 관리해주는 봇을 만들어 볼꺼임

image-2

신규 애플리케이션을 선택하고 이름을 입력하면 애플리케이션이 생김 이후 봇 토큰과 인증 토큰을 잘 복사해두면 일단 봇 생성은 잘 끝난것임

일반 정보 탭에서 봇 기본 프로필 설정하기

image-3

사이드바는 다음과 같이 구성되어 있는데 우리는 일단 일반 정보 탭에서 디스코드 기본 정보를 넣어보자.

image-4

앱 이름과 설명을 넣고 Application ID는 수 많은 공개 봇 중에서 내 봇을 구별할 때 사용하는 것이고 퍼블릭 키는 HTTP 요청이 진짜 디스코드에서 온 건지 확인할 때 사용하는 것임

봇 탭

image-5

아이콘이나 배너 설정할 수 있고 사용자명은 디스코드에서 표시되는 봇 이름임 토큰은 생성하고 처음 복사할 수 있고 이후에는 사진처럼 토큰이 안나오니까 저장해두기

image-6

그리고 이런 추가적인 기능들이 존재함.

설치 탭

여기서는 봇을 어디에 설치할 수 있고 어떤 권한을 부여할 수 있고 서버 초대 링크는 어떻게 만들지 정할 수 있음

image-7

  • Guild install - 디스코드 서버에 봇을 설치하고 서버 채널을 관리할 수 있게 함
  • User install - 개인 계정에 앱을 추가해서 나만 사용할 수 있는 명령어들이나 그런것들을 가능하게 함

image-8

여기서는 디스코드가 공식 서버 초대 링크를 자동으로 생성하게 하는것이 제일 편함

image-9

우리는 길드 설치니까 스코프 부터 보면 다음과 같은 항목들이 존재하는데 2개 다 활성화 하면 됨

  • bot - 디스코드 서버에 봇 계정 추가
  • applications.commands - /명령어 사용 가능하게 해줌

그리고 permissions 를 보면

image-10

굉장히 많은데 필요한것들만 활성화 해주면 됨

봇 초대

설치 탭에서 설치 링크를 브라우저에 복사 붙여넣기 하면 다음과 같이 추가하고 싶은 서버에 추가가 가능함

be738d5c-fb7c-4d9f-b7a3-8bd0a19c1665

명령어 및 기능 구현하기

봇 실행 환경 구축하기

사실 이후에는 코덱스한테 만들어달라고 하긴 했는데 직접 조금 해봤음

디스코드 앱이 동작하려면 내 컴퓨터에서 디스코드 앱 동작 로직을 짜서 구동시켜 줘야한다. 나는 WSL Ubuntu26.04 환경에서 구축했고 다음과 같이 라이브러리를 다운로드 함.

1
python -m pip install -U discord.py python-dotenv

.env 파일에 디스코드 Bot 탭에서 받은 토큰을 준비하고 테스트 서버에 앱을 초대해놓으면 됨

대충 처음 초대해서 채널이나 그런거 다 설정하도록 하는 코드는 이런 느낌임 구현은 당연히 AI가 해줬고 앞으로 기능은 모두 설계도만 내가 만들고 구현은 모두 AI한테 해달라고 할꺼임

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
export const botPermissions =
  P.ViewChannel |
  P.SendMessages |
  P.ReadMessageHistory |
  P.ManageChannels;

export class LiveDiscord implements DiscordAdapter {
  client = new Client({
    intents: [GatewayIntentBits.Guilds],
    allowedMentions: { parse: [] },
    rest: {
      timeout: 20000,
      rejectOnRateLimit: data =>
        data.method === "PATCH" && data.route === "/channels/:id",
    },
  });

  constructor(public db: PrismaClient) {}

}


async mapping(gid: string, key: string) {
  return (
    await this.db.guildChannelMapping.findUnique({
      where: { guildId_key: { guildId: gid, key } },
    })
  )?.discordId;
}

async save(gid: string, key: string, id: string) {
  await this.db.guildChannelMapping.upsert({
    where: { guildId_key: { guildId: gid, key } },
    create: { guildId: gid, key, discordId: id },
    update: { discordId: id },
  });
}

async category(g: Guild, key: string, name: string, aliases: string[] = []) {
  const mapped = await this.mapping(g.id, `cat:${key}`);
  let c = mapped ? g.channels.cache.get(mapped) : undefined;

  if (!c) {
    c = g.channels.cache.find(v =>
      v.type === ChannelType.GuildCategory &&
      [name, ...aliases].includes(v.name)
    );
  }

  if (!c) {
    c = await g.channels.create({
      name,
      type: ChannelType.GuildCategory,
    });
  }

  if (c.type !== ChannelType.GuildCategory) {
    throw new AppError(409, "저장된 카테고리 매핑의 유형이 다릅니다.");
  }

  if (c.name !== name) await c.setName(name);

  await this.save(g.id, `cat:${key}`, c.id);
  return c;
}

async channel(
  g: Guild,
  key: string,
  name: string,
  parent: string,
  type: ChannelType.GuildText | ChannelType.GuildVoice,
  aliases: string[] = [],
) {
  const mapped = await this.mapping(g.id, key);
  let c = mapped ? g.channels.cache.get(mapped) : undefined;

  if (!c) {
    c = g.channels.cache.find(v =>
      v.type === type && [name, ...aliases].includes(v.name)
    );
  }

  if (!c) c = await g.channels.create({ name, type, parent });

  if (c.type !== type) {
    throw new AppError(409, "저장된 채널 매핑의 유형이 다릅니다.");
  }

  if (c.parentId !== parent) {
    await c.setParent(parent, { lockPermissions: false });
  }
  if (c.name !== name) await c.setName(name);

  await this.save(g.id, key, c.id);
  return c;
}