Портал разработчиков
Экспериментально Платформа для ботов и приложений активно развивается. Поддержка собственных серверов вышла 13.08.2026, и функций там пока меньше, чем в облаке. Посмотрите, что поддерживается.
← Документация

Настройки в приложении

GameVox умеет отрисовывать форму настроек вашего приложения прямо в клиенте. Приложение описывает форму через шлюз, оператор редактирует её в разделе Настройки сервера ▸ Интеграции ▸ Настройки, и изменённые значения возвращаются прямо вашему боту. GameVox ничего из этого не хранит: ваше приложение остаётся единственным владельцем своей конфигурации.

У этой функции нет аналога в Discord. Это единственная часть платформы приложений GameVox, которая намеренно несовместима, потому что на другой стороне просто не с чем быть совместимой.

Окно настроек сервера в клиенте GameVox: вкладка «Настройки» установленного приложения с выпадающим списком языка, переключателем и списком каналов с флажками.
Живая панель, целиком отрисованная из одного ответа бота: select, boolean и выбор channels, сгруппированные в раздел с описанием. Бот не отправлял ни одного id канала: этот список предоставил GameVox. Названия сервера и каналов скрыты.

Зачем это нужно

В Discord бот с хоть какими-то настройками поставляется с веб-дашбордом: домен, вход через OAuth2, работа с сессиями, проверка прав с повторным чтением списка серверов пользователя, защита от CSRF и хостинг для всего этого. И обычно лишь затем, чтобы оператор выбрал канал для логов и переключил три функции. GameVox уже знает, кто оператор, какой сервер он настраивает и имеет ли он на это право, поэтому предоставляет такую панель вашему приложению напрямую.

DiscordGameVox
Где операторы настраивают приложение Веб-дашборд, который хостите вы Настройки сервера ▸ Интеграции ▸ Настройки
Что вы разрабатываете Веб-приложение, вход через OAuth2, сессии, проверки прав, хостинг Один обработчик шлюза и один REST-вызов
Кто аутентифицирует оператора Вы, через OAuth2 GameVox, ещё до того, как запрос дойдёт до приложения
Кто хранит конфигурацию Вы Вы. GameVox ничего не сохраняет.
Выбор каналов и ролей Вы загружаете каналы и роли сервера и отрисовываете их Вы называете тип, варианты предоставляет GameVox
Цена одного переключателя Целый дашборд Около двадцати строк

Это ничего не заменяет. Слэш-команды по-прежнему работают, а если у вас уже есть дашборд, его можно оставить. Одно не исключает другого, и многим приложениям пригодится панель для пяти настроек, которые операторы действительно меняют, и дашборд для всего остального.

Как это работает

Оператор открывает вкладку «Настройки»
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ ваш бот
                                                                │
ваш бот  ─── POST /applications/@me/settings-response ──────────┘
             { nonce, version: 2, sections: [ ... ] }
      │
      ▼
GameVox отрисовывает форму, оператор редактирует её и нажимает «Сохранить»
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ ваш бот
                                                                │
ваш бот  ─── POST /applications/@me/settings-response ──────────┘
             { nonce, message: "Сохранено." }

Два действия, одна структура запроса, один эндпоинт для ответа. describe спрашивает, что приложение предлагает настроить на этом сервере; save передаёт то, что изменил оператор. На оба отвечают одинаково.

1. Получение запроса

GameVox отправляет APP_SETTINGS_REQUEST через шлюз в ту сессию, которая держит соединение вашего приложения. Никакой интент его не ограничивает: событие адресовано именно вашему приложению и не фильтруется по маске интентов, поэтому приходит независимо от того, с какими интентами вы выполнили Identify.

{
  "op": 0,
  "t": "APP_SETTINGS_REQUEST",
  "d": {
    "nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
    "server_id": "1387452901234567890",
    "action": "describe",
    "schema_version": 2
  }
}
ПолеТипПримечания
nonce string Непрозрачное значение. Верните его без изменений: только оно связывает ваш ответ с ожидающим оператором. Ответить можно в течение 15 секунд.
server_id snowflake Сервер (guild) в том же пространстве id, что и во всех остальных событиях. Это тот же id, что пришёл в GUILD_CREATE, поэтому ваш существующий кэш серверов его распознает.
action string "describe" или "save".
schema_version integer Самая богатая схема, которую может отрисовать этот сервер. Сейчас 2. См. Работа со старым сервером.
values object Есть только в save. Ключи совпадают с ключами полей, которые вы отправили.

server_id проставляет GameVox из аутентифицированного запроса самого оператора. Из вашего ответа он никогда не считывается, поэтому приложение не может ответить на вопрос об одном сервере настройками другого.

Чтение события в вашей библиотеке

Ни одна библиотека для ботов не содержит обработчика события, которого нет в Discord, поэтому пакет отбрасывается, не дойдя до обработчика событий. Но каждая библиотека даёт доступ к сырому потоку событий как раз для такого случая:

БиблиотекаХукКак включить
discord.js (v14) client.on('raw', packet) Включено по умолчанию. Вызывается для каждого события до обработки пакета.
discord.py (v2) on_socket_raw_receive(msg) Передайте клиенту enable_debug_events=True.
Eris client.on('rawWS', packet) Включено по умолчанию.
JDA RawGatewayEvent JDABuilder.setRawEventsEnabled(true)
DSharpPlus DiscordClient.UnknownEvent Включено по умолчанию. EventName плюс сырой Json.
serenity RawEventHandler::raw_event ClientBuilder::raw_event_handler

С настоящим Discord этот обработчик никогда не срабатывает, поэтому одна и та же сборка работает на обеих платформах без ветвлений.

2. Ответ

Отвечайте через REST, а не через шлюз. У каждой библиотеки есть HTTP-клиент, но большинство не позволяют коду приложения записывать в сокет произвольные опкоды, поэтому ответ устроен именно так.

POST https://app-api.gamevox.com/api/v10/applications/@me/settings-response
Authorization: Bot YOUR_BOT_TOKEN
Content-Type: application/json
{
  "nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
  "version": 2,
  "message": "Необязательная заметка над формой.",
  "sections": [
    {
      "key": "welcome",
      "label": "Приветственные сообщения",
      "description": "Публикуется, когда кто-то присоединяется.",
      "fields": [
        { "key": "welcome.enabled", "label": "Отправлять приветственное сообщение",
          "type": "boolean", "value": true }
      ]
    }
  ]
}

Успешный ответ возвращает 204 No Content. Можно добавить ?server_id= с id сервера: он используется только для того, чтобы отнести вызов к нужному серверу в журнале активности вашего приложения.

КлючТипПримечания
nonce string Обязательно. Из запроса.
version integer Схема, в которой вы отвечаете. Укажите 2, если отправляете sections.
sections array Форма схемы v2. Нельзя использовать вместе с fields.
fields array Плоская форма схемы v1. Отрисовывается как один раздел без заголовка.
message string В describe это заметка над формой. В save это всплывающее подтверждение. Обрезается до 200 символов.
error string Показывается оператору вместо формы. Обрезается до 300 символов.

Всегда отвечайте, в том числе при ошибке. Если молча проигнорировать запрос, оператор будет смотреть на индикатор загрузки до 12-секундного тайм-аута GameVox, и это выглядит как «приложение сломано», а не как «один раз что-то пошло не так».

3. Типы полей

Двенадцать типов, и список закрыт: неизвестный type отклоняет весь ответ, а не отрисовывается пустым местом.

Тип Элемент управления value, которое вы отправляетеЧто приходит обратно при сохранении
string Однострочное текстовое поле string string ("", если пусто)
text Многострочное поле string string
boolean Переключатель boolean true / false
number Числовое поле number number или null, если поле очищено
select Выпадающий список ваших options значение опции string или null, если не выбрано
multiselect Список с флажками из ваших options массив значений опций массив строк (может быть пустым)
channel Выпадающий список каналов этого сервера snowflake канала строка snowflake или null
channels Список с флажками из каналов этого сервера массив snowflake массив строк snowflake
role Выпадающий список групп этого сервера snowflake роли строка snowflake или null
roles Список с флажками из групп этого сервера массив snowflake массив строк snowflake
color Выбор цвета #rrggbb #rrggbb, присутствует всегда
static Строка текста только для чтения string Никогда не отправляется: ключа нет в values

У поля цвета всегда есть значение, поэтому приложение, которое ничего не сохраняло, при первом сохранении получит #000000. Если нужно «без цвета», добавьте к полю цвета boolean.

4. Атрибуты полей

АтрибутПрименяется кДействие
key все Обязательно. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Уникален в пределах всего ответа, а не раздела.
label все По умолчанию используется ключ. Обрезается до 100 символов.
help все Строка подсказки под элементом управления. Обрезается до 200.
placeholder текстовые поля, number Обрезается до 100.
options select, multiselect Обязательно для этих двух. { value, label, description }, не более 25. Для других типов отбрасывается.
min , max , step number Задаёт поведение поля ввода. Носит рекомендательный характер: перепроверяйте полученное значение.
max_length string, text Ограничивает ввод, но не выше предела платформы для этого типа.
channel_kinds channel, channels Сужает список выбора. text, voice, forum, news, fileshare, header. Неизвестные виды отбрасываются; пустой результат означает все виды.
required все Помечает подпись и заменяет пункт «Нет» в списке выбора на «Выберите…». Только внешний вид: проверяйте это при сохранении.
disabled все Делает элемент серым. Всё равно отправляется с текущим значением.
secret string, text Маскирует ввод и не передаёт текущее значение клиенту. С любым другим типом ответ отклоняется.
show_if все { key, equals }. Скрывает поле, пока другое поле в том же ответе не примет указанное значение.

show_if это проверка на равенство и больше ничего: никаких выражений и операторов. Указанный ключ должен принадлежать полю с одним значением в том же ответе и не может быть ключом самого поля; условие, нарушающее любое из этих правил, отбрасывается, и поле показывается всегда. equals может быть строкой, boolean, числом или null.

Скрытое поле всё равно отправляется. У него есть значение, которое прислало ваше приложение, и если его отбросить, приложение воспримет это как очистку поля оператором.

5. Выбор каналов и ролей

В этой части стоит разобраться, потому что она переворачивает привычную схему. Ваше приложение называет вид, а варианты предоставляет GameVox.

Поле канала не содержит id. Вы отправляете { "type": "channel" } и, при желании, фильтр channel_kinds. GameVox прикладывает собственный список каналов этого сервера, клиент отрисовывает по нему список выбора, и оператор может отправить только вариант, выданный GameVox.

Каждая запись каталога имеет в качестве ключа тот же snowflake, который ваше приложение уже видит для этого канала или группы. Нет шага перевода id, в котором можно ошибиться: оператор выбирает подпись, приложение получает id в своём пространстве имён, а значение, указывающее на что-то вне этого сервера, просто не совпадает ни с одним вариантом.

Поле channels, отрисованное как список с флажками из собственных каналов сервера, у каждого значок его вида.
Всё, что приложение отправило для этого поля: { "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. Список, значки видов и id предоставил GameVox.
СитуацияЧто видит оператор
Ничего ещё не сохранено Выбран пункт «Нет». В поле с required вместо него показывается «Выберите…», чтобы при сохранении не записалось значение, которое никто не выбирал.
Сохранённого id больше нет (канал удалён) Остаётся как «Текущий выбор (больше нет в списке)» и по-прежнему выбран. Сохранение не очищает его незаметно.
В списке с несколькими значениями есть сохранённые id, которых больше нет в списке Отрисовываются и отмечены рядом с актуальными по той же причине.
Каталог недоступен (собственный сервер не в сети) Неактивный список с надписью «Сейчас недоступно». При сохранении возвращается сохранённое значение, поэтому открытие панели во время сбоя не может стереть настройку.
На сервере больше 500 каналов или групп Список обрезается до 500, чтобы не пересылать мегабайт вариантов.

Из каталога ролей исключены две вещи: группа владельца сервера, которую ни одному приложению не следует предлагать как назначаемую роль, и группы отдельных приложений, которые несут собственные права установленных приложений.

6. Сценарии

Сценарий 1. Самая маленькая полезная панель

Один переключатель, без разделов. Плоский массив fields допустим в любой версии схемы и отрисовывается как одна группа без заголовка.

{
  "nonce": nonce,
  "fields": [
    { "key": "greetings", "label": "Приветствовать новых участников", "type": "boolean", "value": true }
  ]
}

Сценарий 2. Группировка по разделам

{
  "nonce": nonce,
  "version": 2,
  "sections": [
    {
      "key": "general",
      "label": "Общие",
      "description": "Как бот ведёт себя на этом сервере.",
      "fields": [
        { "key": "prefix", "label": "Префикс команд", "type": "string",
          "value": "!", "max_length": 4, "help": "Используется для устаревших текстовых команд." }
      ]
    },
    {
      "key": "logging",
      "label": "Журнал аудита",
      "fields": [
        { "key": "log.enabled", "label": "Записывать действия модерации", "type": "boolean", "value": false }
      ]
    }
  ]
}

key раздела необязателен, но если указан, должен быть уникальным. label и description тоже необязательны; раздел без полей не отрисовывается.

Сценарий 3. Запрос канала

Никаких id и никакой загрузки списка каналов сервера. Ограничьте выбор видами каналов, в которых могут быть сообщения.

{
  "key": "log.channel",
  "label": "Канал для логов",
  "type": "channel",
  "value": stored.logChannelId ?? null,
  "channel_kinds": ["text", "news"],
  "help": "Куда записываются действия модерации."
}

Используйте "channel_kinds": ["header"], если нужна категория, а не место для публикации: заголовки (headers) это категории GameVox.

Сценарий 4. Условное отображение полей

Обычная схема: boolean, от которого зависит остальная часть функции.

"fields": [
  { "key": "welcome.enabled", "label": "Отправлять приветственное сообщение",
    "type": "boolean", "value": true },

  { "key": "welcome.channel", "label": "Канал", "type": "channel",
    "value": stored.welcomeChannel ?? null,
    "channel_kinds": ["text", "news"],
    "show_if": { "key": "welcome.enabled", "equals": true } },

  { "key": "welcome.message", "label": "Сообщение", "type": "text",
    "value": stored.welcomeMessage ?? "",
    "max_length": 1800,
    "placeholder": "Добро пожаловать на {server}, {user}!",
    "help": "{user}, {server} и {membercount} подставляются при отправке.",
    "show_if": { "key": "welcome.enabled", "equals": true } }
]

Условия могут зависеть и от select, и так делается переключатель режимов: "show_if": { "key": "mode", "equals": "advanced" }.

Сценарий 5. Ваши собственные варианты

select и multiselect это два типа, которые несут собственные варианты. Оба требуют хотя бы одного варианта и допускают не более 25.

{
  "key": "automod.action",
  "label": "Когда срабатывает фильтр",
  "type": "select",
  "value": stored.action,
  "required": true,
  "options": [
    { "value": "delete",  "label": "Удалить сообщение" },
    { "value": "warn",    "label": "Предупредить автора",  "description": "Удаляет сообщение и пишет участнику в ЛС." },
    { "value": "timeout", "label": "Выдать автору тайм-аут", "description": "10 минут." },
    { "value": "none",    "label": "Ничего не делать" }
  ]
}
{
  "key": "automod.filters",
  "label": "Активные фильтры",
  "type": "multiselect",
  "value": stored.filters,
  "options": [
    { "value": "invites",   "label": "Приглашения Discord/GameVox" },
    { "value": "links",     "label": "Ссылки" },
    { "value": "mentions",  "label": "Массовые упоминания" },
    { "value": "caps",      "label": "Избыток заглавных букв" }
  ]
}

description варианта показывается после подписи в выпадающем списке и как всплывающая подсказка в списке с флажками.

Сценарий 6. Роли

То же правило каталога, что и для каналов. GameVox перечисляет группы этого сервера по рангу, с их цветами.

"fields": [
  { "key": "autorole.role", "label": "Роль при входе",
    "type": "role", "value": stored.autoRole ?? null },

  { "key": "moderator.roles", "label": "Роли, которым доступны команды модерации",
    "type": "roles", "value": stored.modRoles,
    "help": "Участники с любой из этих ролей могут использовать /ban и /timeout." }
]

Сценарий 7. Числа с границами

{
  "key": "automod.threshold",
  "label": "Сообщений до объявления рейда",
  "type": "number",
  "value": stored.threshold ?? 10,
  "min": 3,
  "max": 100,
  "step": 1,
  "help": "Считается в скользящем окне 60 секунд."
}

min, max и step влияют только на элемент управления. Браузер оператора не является валидатором под вашим контролем, поэтому снова ограничьте значение, когда оно вернётся. И учтите, что очистка поля отправляет null, а не 0.

Сценарий 8. Секреты

Пометьте учётные данные как secret, и GameVox не передаст клиенту их текущее значение. Его нет ни в кадре WebSocket, ни в DOM, ни на скриншоте панели: оператор видит пустое маскированное поле с подсказкой «Без изменений».

{
  "key": "integrations.apiKey",
  "label": "API-ключ погодного сервиса",
  "type": "string",
  "secret": true,
  "help": "Оставьте пустым, чтобы сохранить текущий ключ."
}

Отсюда вытекает правило: пустой отправленный секрет означает «оставить без изменений», а не «очистить». Если оператору нужно удалить учётные данные, дайте ему для этого отдельный boolean. secret на любом типе, кроме string или text, отклоняет ответ.

Сценарий 9. Статус только для чтения

static отрисовывает строку текста и никогда не отправляется. Используйте его для состояния, которое оператору нужно видеть во время настройки, но нельзя изменить здесь.

"fields": [
  { "key": "plan", "label": "Тариф", "type": "static", "value": "Pro, 4000 запросов в день" },
  { "key": "usage", "label": "Использовано сегодня", "type": "static", "value": "1284 запроса" },
  { "key": "lastSync", "label": "Последняя синхронизация", "type": "static", "value": "2026-09-02 14:31 UTC" }
]

Сценарий 10. Обработка сохранения

Запрос на сохранение содержит values с ключами полей, которые вы отправили. Сохраните данные и ответьте подтверждением.

// action === "save"
const v = req.values ?? {};

const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Этого бота больше нет на этом сервере." });

const patch = {};

// boolean: всегда настоящий boolean
patch.welcomeEnabled = v["welcome.enabled"] === true;

// channel: строка snowflake или null; перепроверьте её для ЭТОГО сервера
const chan = v["welcome.channel"];
patch.welcomeChannel =
  (typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;

// number: число или null, если оператор очистил поле
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;

// secret: пусто значит без изменений
const key = v["integrations.apiKey"];
if (typeof key === "string" && key.trim() !== "") patch.apiKey = key.trim();

await store.update(req.server_id, patch);
await reply(nonce, { message: "Настройки сохранены." });

Ваше message становится всплывающим подтверждением для оператора. Форма остаётся ровно такой, какой он её оставил, так что можно продолжать редактирование.

Сценарий 11. Отклонение сохранения

Верните error, и оператор увидит ваш текст вместо подтверждения. Используйте это для всего, что вы не можете принять: значение не прошло вашу проверку, превышен лимит тарифа, внешние учётные данные больше не работают.

// `required` влияет только на внешний вид, проверяйте его здесь.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
  return reply(nonce, guildId, {
    error: "Выберите, что должен делать AutoMod при срабатывании фильтра.",
  });
}

// Всё, что может знать только ваша сторона.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
  return reply(nonce, guildId, {
    error: "Погодный сервис отклонил этот API-ключ.",
  });
}

То же касается describe: если ваша база данных недоступна, ответьте с error, а не молчите. «Приложению сейчас не удалось прочитать свои настройки» для оператора гораздо лучше, чем двенадцать секунд индикатора загрузки.

Сценарий 12. Работа со старым сервером

Запрос содержит schema_version, так что гадать не придётся. Отдавайте самую богатую форму, которую сервер действительно сможет отрисовать.

const version = typeof req.schema_version === "number" ? req.schema_version : 1;

if (version >= 2) {
  await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
  // v1: плоский список, четыре типа: string, boolean, number, select
  await reply(nonce, { fields: describeV1(guildId) });
}

Верно и обратное. Ответ v2, попавший к старому клиенту, всё равно отрисуется: GameVox отправляет вместе с sections «сплющенный» массив fields, а клиент, не знающий новых типов, показывает их как текстовые поля. С потерями, но ничего не скрыто.

Сценарий 13. Собственные серверы

Работает без изменений. На собственном сервере каталоги каналов и ролей запрашиваются с машины владельца сервера, а не из облачных таблиц, и для вашего приложения это незаметно: те же типы полей, те же snowflake, тот же ответ.

Единственное заметное отличие: если сервер недоступен, списки выбора приходят пустыми и отображаются как «Сейчас недоступно». Сохранённые значения приложения возвращаются при сохранении, поэтому ничего не теряется. Не считайте пустой список выбора признаком того, что оператор очистил поле.

7. Что GameVox проверяет, а что нет

GameVox проверяет структуру сохранения и принципиально не проверяет её смысл. Он не хранит отрисованную форму (весь смысл в том, чтобы не хранить вашу конфигурацию), поэтому к моменту прихода сохранения на нашей стороне никто не знает, какой ключ был выбором канала, а какой свободным текстовым полем.

GameVox гарантируетВам всё равно нужно проверить
Ключи соответствуют допустимому набору символов и не являются __proto__, constructor или prototype Что это ключ, который вы действительно отправляли
Значения: null, boolean, число, строка или массив строк, никогда не вложенный объект Что тип соответствует объявленному полю
Длина строк ограничена (2000 символов), а массивы содержат не более 25 элементов Ваши собственные, более строгие ограничения
Каждый id из списка выбора выдан GameVox из каталога этого сервера Что id всё ещё существует на этом сервере: каталог это снимок, и канал могут удалить между отрисовкой и сохранением
Оператор является владельцем сервера или имеет право «Управление сервером», и приложение установлено здесь Любую вашу собственную авторизацию (уровень тарифа, привязанный аккаунт)

Это не новая брешь, которую открывает панель: оператор и так может отправить что угодно POST-запросом на собственный дашборд приложения. Таков контракт, и именно поэтому списки выбора выдают вам id, созданные GameVox, а не позволяют приложению решать, что означает id.

8. Ограничения

Здесь два типа поведения, и разница важна: превышение количества и нарушение структуры отклоняют весь ответ, чтобы вы об этом узнали; превышение длины текста тихо обрезается, чтобы оператор никогда не увидел сломанную вёрстку.

ОграничениеЗначениеПри превышении
Разделов в ответе 12 Отклоняется
Полей в ответе (всего, а не на раздел) 60 Отклоняется
Вариантов в select / multiselect 1–25 Отклоняется
Выбранных элементов в поле с несколькими значениями 25 Отклоняется
Ключ поля / раздела 64 символа Отклоняется
Значение варианта 64 символа Отклоняется
Тело ответа 256 КБ 413
Подпись, текст-заполнитель 100 символов Обрезается
Текст подсказки 200 символов Обрезается
Описание раздела 300 символов Обрезается
message 200 символов Обрезается
error 300 символов Обрезается
Значение поля семейства string 1000 символов Обрезается
Значение поля text / static 2000 символов Обрезается
Каналов или ролей в каталоге 500 Усекается

Об отклонении оператору сообщается так: «Это приложение прислало настройки, которые GameVox не смог прочитать: <причина>», с указанием превышенного предела и поля, которое его превысило. Так же отклоняются повторяющиеся ключи, неизвестный тип, select без вариантов и одновременная отправка sections и fields.

Тихо исправляется, а не отклоняется

  • Текущее value, не подходящее к своему типу, становится пустым. Устаревшее значение должно очистить одно поле, а не обрушить всю панель.
  • options у типа, который их не использует, отбрасываются.
  • min / max / step у нечислового поля отбрасываются.
  • Нераспознанные элементы channel_kinds отбрасываются.
  • show_if, указывающий на неизвестный ключ, на само поле или на поле с несколькими значениями, отбрасывается, и поле показывается всегда.

9. Тайминги и сценарии отказа

ПоведениеЗначениеЧто происходит
Окно ответа 12 с Оператор видит сообщение «Это приложение не ответило. Возможно, оно не поддерживает настройки в приложении».
Время жизни nonce 15 с Опоздавший ответ отбрасывается, а не доставляется в никуда. Ответ с истёкшим nonce возвращает 404.
Задержка между запросами 2 с на действие, на приложение, на сервер «Дайте приложению немного времени и попробуйте снова». У describe и save отдельные окна, поэтому открытие панели не мешает сразу сохранить.
Приложение не в сети - Сразу отклоняется: «Это приложение не в сети, поэтому его сейчас нельзя настроить». Событие не отправляется.
Отвечает не то приложение - 403. Nonce привязан к приложению, для которого он выдан.
Повторный ответ - Засчитывается первый ответ, второй ничего не делает.

Каждый запрос превращается в событие шлюза для вашего приложения, поэтому задержка нужна, чтобы оператор не мог через GameVox заваливать запросами чужого бота. Планируйте обработчик describe соответственно: он выполняется при открытии страницы, чтение из базы данных вполне ожидаемо, но двенадцать секунд это жёсткий предел.

10. Полный пример (discord.js)

Готов к использованию, не хватает только вашего слоя хранения. Это та же структура, что и в эталонной реализации.

const SCHEMA_V2 = 2;
const POSTABLE = ["text", "news", "forum", "fileshare"];

client.on("raw", (packet) => {
  if (!packet || packet.t !== "APP_SETTINGS_REQUEST") return;
  void handleSettings(packet.d ?? {});
});

async function reply(nonce, guildId, body) {
  await client.rest.post("/applications/@me/settings-response", {
    body: { nonce, ...body },
    query: new URLSearchParams({ server_id: guildId }),
  });
}

async function handleSettings(req) {
  const nonce = typeof req.nonce === "string" ? req.nonce : "";
  const guildId = typeof req.server_id === "string" ? req.server_id : "";
  if (!nonce || !guildId) return;

  try {
    if (req.action === "save") {
      const message = await applySettings(guildId, req.values ?? {});
      return await reply(nonce, guildId, { message });
    }

    const version = typeof req.schema_version === "number" ? req.schema_version : 1;
    if (version >= SCHEMA_V2) {
      return await reply(nonce, guildId, {
        version: SCHEMA_V2,
        sections: await describe(guildId),
      });
    }
    return await reply(nonce, guildId, { fields: await describeLegacy(guildId) });
  } catch (err) {
    // Отвечайте даже при ошибке: молчание выглядит как сломанное приложение.
    await reply(nonce, guildId, {
      error: "Боту не удалось прочитать свои настройки для этого сервера.",
    }).catch(() => undefined);
  }
}

async function describe(guildId) {
  const cfg = await store.get(guildId);
  return [
    {
      key: "general",
      label: "Общие",
      description: "Как бот ведёт себя на этом сервере.",
      fields: [
        { key: "prefix", label: "Префикс команд", type: "string",
          value: cfg.prefix, max_length: 4 },
        { key: "ignored", label: "Игнорируемые каналы", type: "channels",
          value: cfg.ignored, channel_kinds: POSTABLE,
          help: "В этих каналах бот не отвечает на команды." },
      ],
    },
    {
      key: "welcome",
      label: "Приветственные сообщения",
      description: "Публикуется, когда кто-то присоединяется.",
      fields: [
        { key: "welcome.enabled", label: "Отправлять приветственное сообщение",
          type: "boolean", value: cfg.welcome.enabled === true },
        { key: "welcome.channel", label: "Канал", type: "channel",
          value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
          show_if: { key: "welcome.enabled", equals: true } },
        { key: "welcome.message", label: "Сообщение", type: "text",
          value: cfg.welcome.message ?? "", max_length: 1800,
          placeholder: "Добро пожаловать на {server}, {user}!",
          show_if: { key: "welcome.enabled", equals: true } },
      ],
    },
  ];
}

async function applySettings(guildId, v) {
  const guild = client.guilds.cache.get(guildId);
  if (!guild) throw new Error("not in guild");

  const channel = (id) =>
    typeof id === "string" ? (guild.channels.cache.has(id) ? id : null) : null;

  await store.set(guildId, {
    prefix: String(v.prefix ?? "!").slice(0, 4) || "!",
    ignored: Array.isArray(v.ignored) ? v.ignored.filter(channel).slice(0, 25) : [],
    welcome: {
      enabled: v["welcome.enabled"] === true,
      channel: channel(v["welcome.channel"]),
      message: String(v["welcome.message"] ?? "").slice(0, 1800),
    },
  });

  return "Настройки сохранены.";
}

11. Чек-лист перед выпуском

  • Каждый ключ поля стабилен. Переименование ключа теряет всё, что оператор уже настроил.
  • Каждый id из списка выбора перепроверяется для сервера перед записью.
  • Пустой secret считается неизменённым, а не очищенным.
  • Путь describe отвечает заметно быстрее 12 секунд, включая чтение из базы данных.
  • Каждый путь ошибки всё равно отвечает: с error, а не молчанием.
  • required и min/max повторно проверяются при сохранении.
  • Пустой список выбора означает, что каталог был недоступен, а не что оператор очистил поле.
  • В Discord обработчик ничего не делает, поэтому одна сборка работает на обеих платформах.

Кто может открыть панель

Владелец сервера или участник с правом «Управление сервером»: то же условие, что и для установки и удаления приложений. На собственном сервере проверка выполняется на машине владельца сервера, поэтому администратору там не откажут из-за отсутствия строки с правами в облаке.

GameVox сразу отклоняет запрос, если ваше приложение не установлено на этом сервере, поэтому запрос панели может попасть только к приложению, которое оператор уже авторизовал.

← Переход с Discord  ·  Назад к документации