Обложка: Discord-бот на Python для автоматизации работы с Unity Cloud Build в GameDev команде

Discord-бот на Python для автоматизации работы с Unity Cloud Build в GameDev команде

Александр Пиндык

Александр Пиндык

Senior Software Engineer в Wargaming.net

Я расскажу о создании Discord-бота на Python, который запускает сборку проекта в Unity Cloud Build и создаёт ссылку на скачивание для QA из внешней команды.

Настройка и поддержка полноценного CI занимает много времени и средств. При этом команде чаще всего нужна базовая функциональность. Так случилось и с нашим проектом. Потребовался простой способ сборки проекта в Unity Cloud Build и передачи его на внешний QA.

На начальном этапе были такие условия и ограничения:

  • репозиторий проекта — Bitbucket (есть поддержка GitHub и GitLab);
  • наличие лицензии Unity Teams Advanced, которая разблокирует доступ к Unity Cloud Build;
  • ограниченный доступ к Unity Cloud Build для части команды;
  • сборка версии проекта не после каждого коммита в ветку, а только после завершения работы над функциональностью или багом.

Для этих целей был создан Discord-бот на Python (этот язык выбрал, как самый быстрый и удобный для автоматизации). Он позволил команде запускать сборку версии в Unity Cloud Build, и после её успешного завершения формировал ссылку для скачивания QA, как внутри команды, так и вне её.

Боты для Discord на Python: проект для начинающих

Перед тем, как начать писать бот, необходимо подготовить окружение. Для этого настроим Unity Cloud Build и Discord-гильдию (сервер) на сборку проекта под разные платформы. После этого настроим отправку сообщений об успешном завершении сборки из Unity Cloud Build в канал. И создадим бота, работающего с Unity Cloud Build и Discord.

Настройка проекта в Unity Cloud Build

Переходим в Unity Dashboard: https://dashboard.unity3d.com/landing и выбираем Cloud Build:

Далее выбираем Create project и вводим имя нашего проекта (для примера, назовем его Dungeon Crawl Prototype):

После этого в списке проектов появится созданный проект. Переходим к нему и выбираем SET UP CLOUD BUILD:

На следующей странице выбираем с каким Git репозиторием будем интегрировать наш проект. В нашем случае это GitHub:

Подключаем GitHub и переходим к настройке сборки проекта:

Добавляем новую сборку SETUP NEW TARGET:

Выбираем платформу. В нашем примере было создано две сборки под Windows 64 и Android:

Вводим название Target Label, в Branch ветку в git репозитории. Для тестового примера был установлен флаг Auto Detect Version:

Для примера был выбран Debug ключ и тестовый Bundle ID:

Далее нажимаем NEXT: BUILD, после чего настройка сборки завершена. Те же шаги необходимо повторить и для других платформ. На этом заканчиваем настройку Unity Cloud Build. В будущем понадобится создать Discord-оповещения для старта построения сборки и результата сборки. Сделаем это после настройки сервера.

Создание и настройка Discord-гильдии (сервера)

Переходим по ссылке к Discord https://discord.com/, скачиваем приложение и создаем аккаунт. После того как приложение установлено и произведена авторизация в аккаунт, необходимо создать новый Discord-сервер (далее будем называть их гильдиями). Создаем новую гильдию нажав на  «‎+». Выбираем пункт Create My Own → For me and my friends и вводим название гильдии без пробелов.

После создания сервера необходимо добавить новые роли. Для этого переходим в настройки и создаем три:

  • bot для нашего бота, ci для пользователей:
  • ci для пользователей, которые смогут давать команды боту:
  • qa для тех, кому будет доступна ссылка на скачивания сборки.

Далее создадим три канала:

  • ci для отправки команд боту;
  • build для сообщений от Unity Cloud Build;
  • testing отправки ссылок QA.

На этом настройка гильдии закончена. Следующим шагом будет создание Discord-бота.

Создание и настройка Discord-бота

Боты для Discord создаются на Discord Developer Portal: https://discord.com/developers/applications. Необходимо перейти по ссылке и авторизоваться. Затем создаем новое приложение, нажав на New Application. Вводим имя приложения и нажимаем Create:

Далее создаем бота. Для этого переходим в созданное приложение, выбираем вкладку Bot и нажимаем Add Bot:

Далее необходимо настроить разрешения для бота. Переходим в пункт OAuth2 и выбираем следующие опции:

После выбора всех опций копируем url кнопкой Copy. Затем в браузере переходим по скопированной ссылке, выбираем Discord-гильдию, созданную ранее, и проверяем, выставленные разрешения:

Далее в настройках Discord-гильдии добавляем нашего бота в роль bot, которую создали ранее:

Осталось настроить Discord-оповещения для Unity Cloud Build (о начале и завершении сборки, и её результате):

Настройка Discord оповещения для Unity Cloud Build

Возвращаемся в Unity Cloud Build и переходим в раздел Notifications, в котором выбираем Integrations page:

Далее создаем новую интеграцию, нажав на NEW INTEGRATION:

Выбираем Discord и нажимаем NEXT:

В появившемся окне выбираем события, при которых будут отправляться сообщения в гильдию. Затем нажимаем NEXT:

В появившемся окне авторизации выбираем нашу гильдию и канал, куда приходить сообщения. Нажимаем Authorize:

На этом настройка оповещений и окружения завершена. На следующем этапе создадим Discord-бот на Python.

Создание Discord-бота на Python

Настройка проекта

Для создания бота будем использовать Python 3.9, а для работы с Discord — библиотеку discord.py: https://pypi.org/project/discord.py/. Также понадобится библиотека python-dotenv для получения переменных окружения из .env файла.

Создаём новый проект и добавляем в него requirements.txt с таким содержанием:

aiohttp==3.7.3
python-dotenv==0.15.0
discord.py==1.6.0

Так же создаём файл .env с переменными:

DISCORD_TOKEN=
DISCORD_GUILD=
DISCORD_BOT_NAME=
UNITY_API_KEY=
UNITY_ORGANIZATION_ID=
PROJECT_ID=PROJECT_NAME=

DISCORD_GUILD — название Discord гильдии.

DISCORD_BOT_NAME — имя бота.

DISCORD_TOKEN — можно получить на Discord Developer Portal в разделе Bot, скопировав токен:

UNITY_API_KEY можно получить в настройках Unity Cloud Build:

UNITY_ORGANIZATION_ID и PROJECT_ID так же можно получить в Unity Cloud Build. Для этого открываем конфигурацию сборки и нажимаем на EDIT BASIC INFO:

Далее в строке браузера можно увидеть UNITY_ORGANIZATION_ID и PROJECT_ID:

PROJECT_NAME — имя проекта, который собирается в Unity Cloud Build:

Написание бота

Далее я опишу основные моменты реализации Discord-бота. Создаём класс DiscordBot который наследуется от discord.Client. Он реализует API, позволяющее подключиться к Discord гильдии:

import discord


class DiscordBot(discord.Client):
    def __init__(self):
        super(DiscordBot, self).__init__()

Далее необходимо подключиться к Discord-гильдии:

await self.start('discord bot token', bot=True)

Метод on_ready, позволяет реагировать на событие подключения бота к Discord-гильдии:

async def on_ready(self):
    for guild in self.guilds:
        if guild.name == 'self guild name here':
            print('Connected to the Discord guild')
            return
    print('Fails connect to the Discord guild')
    await self.close()

Для обработки сообщений необходимо реализовать метод on_message:

async def on_message(self, message: discord.Message):
    try:
        # Do not process self messages or messages not from bot supported channels
        message_channel = message.channel.name
        if message.author == self.user or message_channel not in ['ci', 'build']:
            return

        if message_channel == 'build':
            await self._process_build_event(message)
        else:
            # React only for messages that mention bot
            for mention in message.mentions:
                if self.user == mention:
                    await self._process_bot_command(message)
                    return
    except Exception as e:
        print(f'Exception: {e}')

В первой проверке отсекаем все сообщения от самого бота, а также сообщения, пришедшие не из каналов, поддерживаемых ботом. Затем разделяем сообщения на два типа: команды боту и сообщения от Unity Cloud Build. В зависимости от результата передаём сообщение нужному обработчику.

Обработчик сообщений для выполнения команд ботом. Метод _get_command_for_bot читает сообщение, ищет в нем команду и параметры команды. После этого выполняется метод конкретной команды:

async def _process_bot_command(self, message: discord.Message):
    command, params = self._get_command_for_bot(message)
    if command == 'build':
        await self._start_build(message, params)
    elif command == 'build_target_info':
        await self._build_target_info(message, params)
    else:
        print(f'Unsupported command for bot: {command}')

Для работы с Unity Cloud Build создаём класс UnityCloudBuildWorker:

from aiohttp import ClientSession


class UnityCloudBuildWorker(object):
    def __init__(self, base_url: str, project_id: str, api_key: str):
        self._base_url = base_url
        self._project_id = project_id
        self._cloud_build_targets = {
            'qa_windows': 'qa-windows-64-bit',
            'qa_android': 'qa-android'
        }
        self._supported_builds = list(self._cloud_build_targets.keys())
        self._session = None
        self._base_header = {
            'Content-Type': 'application/json',
            'Authorization': f'Basic {api_key}',
        }

    async def start_worker(self):
        assert self._session is None
        self._session = ClientSession()

    async def stop_worker(self):
        if self._session is not None:
            await self._session.close()
            self._session = None

Разберём пример отправки команды на сборку проекта. Для этого в канал «ci» боту отправляется команда @AleksPinGames_DEV_Bot build qa_windows. Она обозначает, что необходимо начать сборку QA Windows.

Обработчик в DiscordBot получает сообщение и набор параметров и отправляет в чат ответ о начале выполнения задачи. Далее в UnityCloudBuildWorker отправляется запрос на сборку проекта через Unity Cloud Build. После получения ответа бот сообщает о том, какая сборка начала строиться и какая ветка использовалась в Git репозитории:

async def _start_build(self, message: discord.Message, params: str):
    await message.channel.send('Send start build command to Unity Cloud Build')
    result = await self._unity_cloud_build_worker.cmd_start_build(params)
    await message.channel.send(result)

UnityCloudBuildWorker получает команду и выполняет метод cmd_start_build. Затем формируется url запроса и выполняется запрос. Полное описание API для работы с Unity Cloud Build можно найти тут: https://build-api.cloud.unity3d.com/docs/1.0.0/index.html#intro:

async def cmd_start_build(self, build_target: str) -> str:
    build_target_id = self._cloud_build_targets.get(build_target, None)
    if build_target_id is None:
        return f'Unsupported build target: {build_target}'

    url = f'{self._base_url}/projects/{self._project_id}/buildtargets/{build_target_id}/builds'
    headers = {'clean': 'true'}
    response = await self._send(self._send_post, url, headers)
    result = await response.json() if response is not None else {}
    self._logger.info(f'{self._log_tag} cmd_start_build result: {result}')
    if result and len(result) > 0:
        result_msg = f"Start building {result[0]['buildTargetName']} | Branch: {result[0]['scmBranch']}"
    else:
        result_msg = f'Error starting build for target: {build_target}'
    return result_msg

async def _send(self, send_method: callable, url: str, headers: dict = None):
    try:
        if headers is not None:
            headers.update(self._base_header)
        else:
            headers = self._base_header
        result = await send_method(url, headers)
        return result
    except Exception as e:
        print(f'Exception: {e}')
        return None

async def _send_get(self, url: str, headers: dict = None):
    return await self._session.get(url=url, headers=headers, ssl=True)

async def _send_post(self, url: str, headers: dict = None):
    return await self._session.post(url=url, headers=headers, ssl=True)

После сборки проекта Unity Cloud Build отправляет в канал build сообщение о результате сборки. На это сообщение реагирует обработчик сообщений от Unity Cloud Build.

Обработчик сообщений от Unity Cloud Build проверяет от кого пришло сообщение. Все сообщения от Unity с данными по сборке вычитываются и формируется ссылка для канала QA testing. Сообщение в канал QA попадёт только в случае build_success = True. Также в данном обработчике удаляется ссылка на скачивание собранного проекта непосредственно в Unity Cloud Build. Остаётся только внешняя ссылка, не требующая авторизации в Unity Team.

async def _process_build_event(self, message: discord.Message):
        try:
            if message.author.name != 'Unity' or len(message.embeds) <= 0:
                return

            to_delete_idx = None
            build_success = False
            embed = message.embeds[0]
            for idx, field in enumerate(embed.fields):
                field_name = field.name
                if field_name == 'Build success':
                    build_success = True
                elif field_name == 'Download':
                    to_delete_idx = idx

            if build_success:
                if to_delete_idx is not None:
                    embed.remove_field(to_delete_idx)
                channel = self._server_text_channels['testing']
                await channel.send(embed=embed)
        except Exception as e:
            print(f'Exception: {e}')

Запуск бота как сервис

Для удобства запуска бота на удаленном сервере мы добавили в репозиторий возможность собрать и запустить бота в качестве Docker-контейнера. Для этого были добавлены Dockerfile:

FROM python:3.9.2-buster

ENV APP_DIR /unity_cloud_build_discord_bot
ENV PYTHONPATH $PYTHONPATH:$APP_DIR

WORKDIR $APP_DIR

COPY requirements.txt requirements.txt

RUN pip install -r requirements.txt
RUN apt-get update && apt-get install -y --no-install-recommends locales locales-all

COPY . $APP_DIR

CMD python app/discord_bot.py

И Makefile, в котором описаны команды up и down для запуска и остановки контейнера с ботом:

TAG ?= latest
CONTAINER_NAME ?= unity_cloud_build_discord_bot
IMAGE_NAME ?= $(CONTAINER_NAME):$(TAG)

build:
   docker build . -t $(IMAGE_NAME)

up: build
   docker run -d \
   --name $(CONTAINER_NAME) \
   --env-file .env \
   $(IMAGE_NAME)

down:
   docker stop $(CONTAINER_NAME)
   docker rm $(CONTAINER_NAME)

Все инструкции и полная версия кода бота доступны в GitHub-репозитории: https://github.com/AleksandrPindyk/unity_cloud_build_discord_bot

Итог

На этом всё 🙂 Мы запустили бот в качестве Docker-контейнера и настроили на Discord-гильдию, где идёт общение разработчиков и внешних QA. После нескольких дней тестирования мы пришли к выводу, что использовать бота удобнее, чем запускать сборку через веб-страницу Unity Cloud Build. В следующей версии добавим поддержку нескольких проектов и разворачивание WebGL-версии сразу на удаленном сервере в Docker-контейнере. Более детальную информацию о возможностях разработки Discord-ботов на Python, можно получить на сайте с официальной документацией: https://discord.com/developers/docs/intro.