Portal deweloperski
Eksperymentalne Platforma botów i aplikacji jest aktywnie rozwijana. Obsługa serwerów hostowanych samodzielnie pojawiła się 13.08.2026 i ma mniej funkcji niż chmura. Zobacz, co jest obsługiwane.
← Dokumentacja

Ustawienia w aplikacji

GameVox może wyświetlić formularz konfiguracji twojej aplikacji w kliencie. Twoja aplikacja opisuje formularz przez gateway, operator edytuje go w Ustawienia serwera ▸ Integracje ▸ Ustawienia, a zmienione wartości wracają prosto do twojego bota. GameVox niczego z tego nie przechowuje: twoja aplikacja pozostaje jedynym właścicielem swojej konfiguracji.

Nie ma to odpowiednika na Discordzie. To jedyna część platformy aplikacji GameVox, która celowo nie jest zgodna, bo po drugiej stronie nie ma niczego, z czym mogłaby być zgodna.

Okno Ustawień serwera w kliencie GameVox z zakładką Ustawienia zainstalowanej aplikacji: lista rozwijana języka, przełącznik i lista kanałów do zaznaczenia.
Działający panel wygenerowany w całości z jednej odpowiedzi bota: select, boolean i selektor channels, zgrupowane w sekcji z opisem. Bot nie wysłał żadnych identyfikatorów kanałów: tę listę dostarczył GameVox. Nazwy serwera i kanałów zostały ukryte.

Po co to jest

Na Discordzie bot z jakąkolwiek konfiguracją wymaga panelu WWW: domeny, logowania OAuth2, obsługi sesji, sprawdzania uprawnień, które ponownie odczytuje listę gildii użytkownika, ochrony CSRF i hostingu dla tego wszystkiego. Zwykle tylko po to, żeby operator mógł wybrać kanał logów i przełączyć trzy funkcje. GameVox już wie, kim jest operator, który serwer konfiguruje i czy ma prawo go konfigurować, więc udostępnia tę warstwę bezpośrednio twojej aplikacji.

DiscordGameVox
Gdzie operatorzy konfigurują twoją aplikację W panelu WWW, który hostujesz Ustawienia serwera ▸ Integracje ▸ Ustawienia
Co musisz zbudować Aplikację WWW, logowanie OAuth2, sesje, sprawdzanie uprawnień, hosting Jeden handler gatewaya i jedno wywołanie REST
Kto uwierzytelnia operatora Ty, przez OAuth2 GameVox, zanim twoja aplikacja zostanie o cokolwiek zapytana
Kto przechowuje konfigurację Ty Ty. GameVox niczego nie zachowuje.
Selektory kanałów i ról Pobierasz kanały i role gildii i sam je wyświetlasz Podajesz typ; GameVox dostarcza opcje
Koszt udostępnienia jednego przełącznika Cały panel Około dwudziestu linijek

To niczego nie zastępuje. Komendy ukośnikowe nadal działają, a jeśli masz już panel, możesz go zachować. Jedno nie wyklucza drugiego i wiele aplikacji zechce używać panelu w kliencie do pięciu ustawień, które operatorzy faktycznie zmieniają, a panelu WWW do całej reszty.

Jak to działa

Operator otwiera zakładkę Ustawienia
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ twój bot
                                                                │
twój bot ─── POST /applications/@me/settings-response ──────────┘
             { nonce, version: 2, sections: [ ... ] }
      │
      ▼
GameVox wyświetla formularz, operator go edytuje i klika Zapisz
      │
      ▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ twój bot
                                                                │
twój bot ─── POST /applications/@me/settings-response ──────────┘
             { nonce, message: "Zapisano." }

Dwie akcje, jeden kształt żądania, jeden endpoint odpowiedzi. describe pyta, co twoja aplikacja udostępnia dla tego serwera; save przekazuje to, co zmienił operator. Na obie odpowiada się tak samo.

1. Odbieranie żądania

GameVox wysyła przez gateway APP_SETTINGS_REQUEST do sesji, która utrzymuje połączenie twojej aplikacji. Nie blokuje go żaden intent: zdarzenie jest adresowane konkretnie do twojej aplikacji, a nie filtrowane maską intentów, więc dociera niezależnie od tego, z czym się zidentyfikowałeś.

{
  "op": 0,
  "t": "APP_SETTINGS_REQUEST",
  "d": {
    "nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
    "server_id": "1387452901234567890",
    "action": "describe",
    "schema_version": 2
  }
}
PoleTypUwagi
nonce string Nieprzezroczysty. Odeślij go bez zmian; to jedyne, co wiąże twoją odpowiedź z czekającym operatorem. Można na niego odpowiedzieć przez 15 sekund.
server_id snowflake Gildia, w tej samej przestrzeni identyfikatorów co każde inne zdarzenie. To identyfikator, który przyszedł w GUILD_CREATE, więc twoja obecna pamięć podręczna gildii go rozpozna.
action string "describe" lub "save".
schema_version integer Najbogatszy schemat, jaki ten serwer potrafi wyświetlić. Obecnie 2. Zobacz Obsługa starszego serwera.
values object Obecne tylko przy save. Klucze to klucze pól, które wysłałeś.

server_id jest ustawiany przez GameVox na podstawie uwierzytelnionego żądania samego operatora. Nigdy nie jest odczytywany z twojej odpowiedzi, więc aplikacja nie może odpowiedzieć na pytanie o jeden serwer ustawieniami dla innego.

Odczytywanie zdarzenia w twojej bibliotece

Żadna biblioteka botów nie ma handlera dla zdarzenia, którego nie ma Discord, więc pakiet jest odrzucany, zanim dotrze do handlera zdarzeń. Każda biblioteka udostępnia surowy strumień zdarzeń właśnie na taką okazję:

BibliotekaHookWłączenie
discord.js (v14) client.on('raw', packet) Domyślnie włączone. Emitowane dla każdego zdarzenia, zanim pakiet zostanie obsłużony.
discord.py (v2) on_socket_raw_receive(msg) Przekaż klientowi enable_debug_events=True.
Eris client.on('rawWS', packet) Domyślnie włączone.
JDA RawGatewayEvent JDABuilder.setRawEventsEnabled(true)
DSharpPlus DiscordClient.UnknownEvent Domyślnie włączone. EventName plus surowy Json.
serenity RawEventHandler::raw_event ClientBuilder::raw_event_handler

Na prawdziwym Discordzie ten handler nigdy się nie uruchamia, więc ta sama wersja działa na obu platformach bez rozgałęzień.

2. Odpowiadanie

Odpowiadaj przez REST, a nie przez gateway. Każda biblioteka udostępnia klienta HTTP; większość nie pozwala kodowi aplikacji wysyłać dowolnych opcodów do gniazda, dlatego odpowiedź ma taki kształt.

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": "Opcjonalna notatka wyświetlana nad formularzem.",
  "sections": [
    {
      "key": "welcome",
      "label": "Wiadomości powitalne",
      "description": "Wysyłana, gdy ktoś dołącza.",
      "fields": [
        { "key": "welcome.enabled", "label": "Wyślij wiadomość powitalną",
          "type": "boolean", "value": true }
      ]
    }
  ]
}

Udana odpowiedź zwraca 204 No Content. Możesz dopisać ?server_id= z identyfikatorem gildii; służy on tylko do przypisania wywołania w dzienniku aktywności twojej aplikacji.

KluczTypUwagi
nonce string Wymagane. Z żądania.
version integer Schemat, w którym odpowiadasz. Wyślij 2, gdy wysyłasz sections.
sections array Formularz w schemacie v2. Wyklucza się z fields.
fields array Płaski formularz w schemacie v1. Wyświetlany jako jedna sekcja bez nagłówka.
message string Przy describe notatka nad formularzem. Przy save powiadomienie z potwierdzeniem. Przycinane do 200 znaków.
error string Pokazywane operatorowi zamiast formularza. Przycinane do 300 znaków.

Zawsze odpowiadaj, także w razie niepowodzenia. Po cichu porzucone żądanie zostawia operatora z kręcącym się kółkiem aż do 12-sekundowego limitu czasu GameVox, co wygląda jak „ta aplikacja jest zepsuta”, a nie „raz coś poszło nie tak”.

3. Typy pól

Dwanaście typów, a lista jest zamknięta: nierozpoznany type powoduje odrzucenie całej odpowiedzi, zamiast wyświetlić puste miejsce.

Typ Kontrolka value, które wysyłaszCo dostajesz z powrotem przy zapisie
string Jednowierszowe pole tekstowe ciąg znaków ciąg znaków ("", gdy puste)
text Pole tekstowe wielowierszowe ciąg znaków ciąg znaków
boolean Przełącznik wartość logiczna true / false
number Pole liczbowe liczba liczba lub null, jeśli wyczyszczone
select Lista rozwijana z twoimi options wartość opcji ciąg znaków lub null, jeśli nieustawione
multiselect Lista do zaznaczenia z twoimi options tablica wartości opcji tablica ciągów znaków (może być pusta)
channel Lista rozwijana kanałów tego serwera snowflake kanału snowflake jako ciąg znaków lub null
channels Lista do zaznaczenia kanałów tego serwera tablica snowflake tablica snowflake jako ciągów znaków
role Lista rozwijana grup tego serwera snowflake roli snowflake jako ciąg znaków lub null
roles Lista do zaznaczenia grup tego serwera tablica snowflake tablica snowflake jako ciągów znaków
color Próbnik koloru #rrggbb #rrggbb, zawsze obecne
static Wiersz tekstu tylko do odczytu ciąg znaków Nigdy nie jest wysyłany: klucza nie ma w values

Pole koloru zawsze ma wartość, więc aplikacja, która nic nie zapisała, dostanie przy pierwszym zapisie #000000. Jeśli potrzebujesz stanu „bez koloru”, połącz próbnik z polem boolean.

4. Atrybuty pól

AtrybutDotyczyDziałanie
key wszystkich Wymagany. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Unikalny w całej odpowiedzi, a nie w obrębie sekcji.
label wszystkich Domyślnie klucz. Przycinany do 100 znaków.
help wszystkich Linia podpowiedzi pod kontrolką. Przycinana do 200.
placeholder pól tekstowych, number Przycinany do 100.
options select, multiselect Wymagany dla tych dwóch. { value, label, description }, maksymalnie 25. Pomijany przy każdym innym typie.
min , max , step number Kształtuje pole. Tylko informacyjnie: sprawdź otrzymaną wartość ponownie.
max_length string, text Ogranicza długość, maksymalnie do limitu platformy dla danego typu.
channel_kinds channel, channels Zawęża selektor. text, voice, forum, news, fileshare, header. Nieznane rodzaje są pomijane; pusty wynik oznacza wszystkie rodzaje.
required wszystkich Oznacza etykietę i zastępuje pozycję „Brak” w selektorze pozycją „Wybierz…”. Tylko wizualnie: egzekwuj to przy zapisie.
disabled wszystkich Wyszarza kontrolkę. Nadal jest wysyłana, z bieżącą wartością.
secret string, text Maskuje pole i czyści bieżącą wartość przed wysłaniem do klienta. Przy każdym innym typie odpowiedź zostaje odrzucona.
show_if wszystkich { key, equals }. Ukrywa pole, dopóki inne pole w tej samej odpowiedzi nie ma tej wartości.

show_if to test równości i nic więcej: bez wyrażeń, bez operatorów. Wskazany klucz musi należeć do pola jednowartościowego w tej samej odpowiedzi i nie może być kluczem tego samego pola; warunek, który nie spełnia któregoś z tych wymogów, jest pomijany, a pole wyświetla się bezwarunkowo. equals może być ciągiem znaków, wartością logiczną, liczbą lub null.

Ukryte pole nadal jest wysyłane. Ma wartość, którą wysłała twoja aplikacja, a jego pominięcie wyglądałoby dla aplikacji tak, jakby operator je wyczyścił.

5. Selektory kanałów i ról

Tę część warto zrozumieć, bo odwraca zwykły układ. Twoja aplikacja podaje rodzaj; GameVox dostarcza opcje.

Pole kanału nie zawiera identyfikatorów. Wysyłasz { "type": "channel" } i opcjonalnie filtr channel_kinds. GameVox dołącza listę kanałów tego serwera, klient wyświetla na jej podstawie selektor, a operator może wysłać wyłącznie opcję wydaną przez GameVox.

Każdy wpis katalogu ma jako klucz ten sam snowflake, który twoja aplikacja już widzi dla danego kanału lub grupy. Nie ma kroku tłumaczenia identyfikatorów, w którym coś mogłoby pójść źle: operator wybiera etykietę, twoja aplikacja dostaje identyfikator w swojej przestrzeni nazw, a wartość wskazująca coś spoza tego serwera po prostu nie pasuje do żadnej opcji.

Pole channels wyświetlone jako lista do zaznaczenia z kanałami serwera, każdy poprzedzony symbolem swojego rodzaju.
Aplikacja wysłała dla tego pola wyłącznie { "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. Lista, symbole rodzajów i identyfikatory pochodzą od GameVox.
SytuacjaCo widzi operator
Nic jeszcze nie zapisano Zaznaczoną pozycję „Brak”. Pole required pokazuje zamiast niej „Wybierz…”, więc zapis nie może zatwierdzić wartości, której nikt nie wybrał.
Zapisany identyfikator już nie istnieje (kanał usunięty) Pozycja zostaje jako „Bieżący wybór (już niedostępny na liście)”, nadal zaznaczona. Zapis nie czyści jej po cichu.
Zapisane identyfikatory w selektorze wielokrotnym, których nie ma już na liście Wyświetlane i zaznaczone obok aktualnych, z tego samego powodu.
Katalog niedostępny (serwer hostowany samodzielnie offline) Wyłączony selektor z komunikatem „Obecnie niedostępne”. Zapisana wartość jest odsyłana przy zapisie, więc otwarcie panelu podczas awarii nie może skasować ustawienia.
Serwer ma więcej niż 500 kanałów lub grup Lista jest obcinana do 500 pozycji, zamiast wysyłać megabajt opcji.

Katalog ról pomija dwie rzeczy: grupę właściciela serwera, której żadna aplikacja nie powinna dostać jako roli do przypisania, oraz grupy poszczególnych aplikacji, które przechowują uprawnienia zainstalowanych aplikacji.

6. Scenariusze

Scenariusz 1: najmniejszy przydatny panel

Jeden przełącznik, bez sekcji. Płaska tablica fields jest poprawna w każdej wersji schematu i wyświetla się jako jedna grupa bez nagłówka.

{
  "nonce": nonce,
  "fields": [
    { "key": "greetings", "label": "Witaj nowych członków", "type": "boolean", "value": true }
  ]
}

Scenariusz 2: grupowanie w sekcje

{
  "nonce": nonce,
  "version": 2,
  "sections": [
    {
      "key": "general",
      "label": "Ogólne",
      "description": "Jak bot zachowuje się na tym serwerze.",
      "fields": [
        { "key": "prefix", "label": "Prefiks komend", "type": "string",
          "value": "!", "max_length": 4, "help": "Używany dla starszych komend tekstowych." }
      ]
    },
    {
      "key": "logging",
      "label": "Dziennik audytu",
      "fields": [
        { "key": "log.enabled", "label": "Zapisuj działania moderacyjne", "type": "boolean", "value": false }
      ]
    }
  ]
}

key sekcji jest opcjonalny, ale jeśli jest podany, musi być unikalny. label i description są opcjonalne; sekcja bez pól nie jest wyświetlana.

Scenariusz 3: prośba o kanał

Bez identyfikatorów i bez pobierania listy kanałów gildii. Ogranicz selektor do rodzajów, które mogą zawierać wiadomości.

{
  "key": "log.channel",
  "label": "Kanał logów",
  "type": "channel",
  "value": stored.logChannelId ?? null,
  "channel_kinds": ["text", "news"],
  "help": "Tu zapisywane są działania moderacyjne."
}

Użyj "channel_kinds": ["header"], gdy chcesz kategorię, a nie miejsce do publikowania: nagłówki to kategorie w GameVox.

Scenariusz 4: warunkowe pokazywanie pól

Typowy układ: wartość logiczna, która odblokowuje resztę danej funkcji.

"fields": [
  { "key": "welcome.enabled", "label": "Wyślij wiadomość powitalną",
    "type": "boolean", "value": true },

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

  { "key": "welcome.message", "label": "Wiadomość", "type": "text",
    "value": stored.welcomeMessage ?? "",
    "max_length": 1800,
    "placeholder": "Witaj {user} na {server}!",
    "help": "{user}, {server} i {membercount} są podstawiane przy wysyłaniu.",
    "show_if": { "key": "welcome.enabled", "equals": true } }
]

Warunki mogą też zależeć od pola select. Tak buduje się przełącznik trybów: "show_if": { "key": "mode", "equals": "advanced" }.

Scenariusz 5: własne opcje

select i multiselect to dwa typy, które mają własne opcje. Oba wymagają co najmniej jednej opcji i pozwalają na maksymalnie 25.

{
  "key": "automod.action",
  "label": "Gdy filtr zostanie dopasowany",
  "type": "select",
  "value": stored.action,
  "required": true,
  "options": [
    { "value": "delete",  "label": "Usuń wiadomość" },
    { "value": "warn",    "label": "Ostrzeż autora",  "description": "Usuwa wiadomość i wysyła członkowi wiadomość bezpośrednią." },
    { "value": "timeout", "label": "Daj autorowi timeout", "description": "10 minut." },
    { "value": "none",    "label": "Nic nie rób" }
  ]
}
{
  "key": "automod.filters",
  "label": "Aktywne filtry",
  "type": "multiselect",
  "value": stored.filters,
  "options": [
    { "value": "invites",   "label": "Zaproszenia Discord/GameVox" },
    { "value": "links",     "label": "Linki" },
    { "value": "mentions",  "label": "Masowe wzmianki" },
    { "value": "caps",      "label": "Nadmiar wielkich liter" }
  ]
}

description opcji pojawia się za etykietą na liście rozwijanej i jako podpowiedź na liście do zaznaczenia.

Scenariusz 6: role

Ta sama zasada katalogu co przy kanałach. GameVox wyświetla grupy tego serwera według rangi, z ich kolorami.

"fields": [
  { "key": "autorole.role", "label": "Rola nadawana po dołączeniu",
    "type": "role", "value": stored.autoRole ?? null },

  { "key": "moderator.roles", "label": "Role, które mogą używać komend moderacyjnych",
    "type": "roles", "value": stored.modRoles,
    "help": "Członkowie z którąkolwiek z nich mogą używać /ban i /timeout." }
]

Scenariusz 7: liczby z ograniczeniami

{
  "key": "automod.threshold",
  "label": "Liczba wiadomości, po której ogłaszany jest raid",
  "type": "number",
  "value": stored.threshold ?? 10,
  "min": 3,
  "max": 100,
  "step": 1,
  "help": "Liczone w przesuwnym oknie 60 sekund."
}

min, max i step kształtują tylko kontrolkę. Przeglądarka operatora nie jest walidatorem, nad którym masz kontrolę, więc ogranicz wartość ponownie, gdy wróci. Pamiętaj też, że wyczyszczenie pola wysyła null, a nie 0.

Scenariusz 8: sekrety

Oznacz dane uwierzytelniające jako secret, a GameVox wyczyści ich bieżącą wartość przed wysłaniem do klienta. Nie ma jej w ramce WebSocket, w DOM ani na zrzucie ekranu panelu: operator widzi puste, maskowane pole z tekstem zastępczym „Bez zmian”.

{
  "key": "integrations.apiKey",
  "label": "Klucz API pogody",
  "type": "string",
  "secret": true,
  "help": "Zostaw puste, żeby zachować obecny klucz."
}

Wynika z tego zasada: pusty wysłany sekret oznacza „pozostaw bez zmian”, nigdy „wyczyść”. Jeśli operator musi usunąć dane uwierzytelniające, daj mu do tego osobne pole logiczne. secret przy typie innym niż string lub text powoduje odrzucenie odpowiedzi.

Scenariusz 9: status tylko do odczytu

static wyświetla wiersz tekstu i nigdy nie jest wysyłany. Używaj go do stanu, którego operator potrzebuje podczas konfiguracji, ale nie może tu edytować.

"fields": [
  { "key": "plan", "label": "Plan", "type": "static", "value": "Pro: 4000 zapytań dziennie" },
  { "key": "usage", "label": "Wykorzystano dziś", "type": "static", "value": "1284 zapytania" },
  { "key": "lastSync", "label": "Ostatnia synchronizacja", "type": "static", "value": "2026-09-02 14:31 UTC" }
]

Scenariusz 10: obsługa zapisu

Żądanie zapisu zawiera values z kluczami pól, które wysłałeś. Zapisz dane, a potem odpowiedz potwierdzeniem.

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

const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Tego bota nie ma już na tym serwerze." });

const patch = {};

// boolean: zawsze prawdziwa wartość logiczna
patch.welcomeEnabled = v["welcome.enabled"] === true;

// channel: snowflake jako string lub null; sprawdź go ponownie w TEJ gildii
const chan = v["welcome.channel"];
patch.welcomeChannel =
  (typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;

// number: liczba lub null, gdy operator wyczyścił pole
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;

// secret: puste oznacza bez zmian
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: "Ustawienia zapisane." });

Twój message staje się powiadomieniem z potwierdzeniem dla operatora. Formularz zostaje dokładnie w takim stanie, w jakim go zostawił, więc może dalej edytować.

Scenariusz 11: odrzucenie zapisu

Zwróć error, a operator zobaczy twój tekst zamiast potwierdzenia. Używaj tego do wszystkiego, czego nie możesz przyjąć: wartości, która nie przechodzi twojej walidacji, limitu planu, zewnętrznych danych uwierzytelniających, które przestały działać.

// `required` jest tylko wizualne: egzekwuj je tutaj.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
  return reply(nonce, guildId, {
    error: "Wybierz, co AutoMod ma zrobić, gdy filtr zostanie dopasowany.",
  });
}

// Wszystko, co może wiedzieć tylko twoja strona.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
  return reply(nonce, guildId, {
    error: "Dostawca prognozy pogody odrzucił ten klucz API.",
  });
}

To samo dotyczy describe: jeśli twoja baza danych nie działa, odpowiedz error, zamiast nie odpowiadać wcale. „Ta aplikacja nie mogła teraz odczytać swoich ustawień” to dla operatora znacznie lepszy wynik niż dwunastosekundowe kręcące się kółko.

Scenariusz 12: obsługa starszego serwera

Żądanie zawiera schema_version, więc nigdy nie musisz niczego sprawdzać na ślepo. Wysyłaj najbogatszy formularz, jaki serwer faktycznie wyświetli.

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

if (version >= 2) {
  await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
  // v1: płaska lista, cztery typy: string, boolean, number, select
  await reply(nonce, { fields: describeV1(guildId) });
}

Działa to też w drugą stronę. Odpowiedź v2, która trafi do starszego klienta, nadal się wyświetli: GameVox wysyła obok sections spłaszczoną tablicę fields, a klient starszy niż nowe typy rysuje je jako pola tekstowe. Z ograniczeniami, ale nic nie jest ukryte.

Scenariusz 13: serwery hostowane samodzielnie

Działa bez zmian. Na serwerze hostowanym samodzielnie katalogi kanałów i ról są pobierane z serwera klienta, a nie z tabel w chmurze, czego twoja aplikacja w ogóle nie zauważy: te same typy pól, te same snowflake, ta sama odpowiedź.

Jedyna różnica, którą możesz zaobserwować: jeśli serwer jest nieosiągalny, selektory przychodzą puste i wyświetlają się jako „Obecnie niedostępne”. Zapisane wartości twojej aplikacji są odsyłane przy zapisie, więc nic nie ginie. Nie traktuj pustego selektora jako „operator go wyczyścił”.

7. Co GameVox sprawdza, a czego nie

GameVox sprawdza kształt zapisu, ale celowo nie jego znaczenie. Nie zachowuje wyświetlonego formularza (nieprzechowywanie twojej konfiguracji to cały sens), więc w chwili, gdy dociera zapis, nic po naszej stronie nie wie, który klucz był selektorem kanału, a który polem tekstowym.

GameVox gwarantujeNadal musisz sprawdzić
Klucze pasują do dozwolonego zestawu znaków i nie są __proto__, constructor ani prototype Czy klucz jest jednym z tych, które faktycznie wysłałeś
Wartości to null, wartość logiczna, liczba, ciąg znaków lub tablica ciągów znaków, nigdy zagnieżdżony obiekt Czy typ pasuje do zadeklarowanego pola
Ciągi znaków są ograniczone (2000 znaków), a tablice mają maksymalnie 25 elementów Twoje własne, ostrzejsze limity
Każdy identyfikator z selektora został wydany przez GameVox z katalogu tego serwera Czy identyfikator nadal istnieje w tej gildii: katalogi to migawka, a kanał może zostać usunięty między wyświetleniem a zapisem
Operator jest właścicielem serwera lub ma uprawnienie Zarządzaj serwerem, a aplikacja jest tu zainstalowana Własną autoryzację (poziom planu, połączone konto)

To nie jest luka wprowadzona przez panel: operator już teraz może wysłać metodą POST cokolwiek do własnego panelu aplikacji. Tak wygląda umowa i właśnie dlatego typy selektorów dają ci identyfikatory wygenerowane przez GameVox, zamiast pozwalać aplikacji decydować, co identyfikator oznacza.

8. Limity

Działają tu dwa zachowania i różnica ma znaczenie: przekroczenie liczby elementów lub błędna struktura powodują odrzucenie całej odpowiedzi, więc się o tym dowiesz; limity tekstu przycinają po cichu, więc operator nigdy nie zobaczy rozsypanego układu.

LimitWartośćPo przekroczeniu
Sekcje w odpowiedzi 12 Odrzucane
Pola w odpowiedzi (łącznie, nie na sekcję) 60 Odrzucane
Opcje w select / multiselect 1–25 Odrzucane
Wybrane elementy w polu wielowartościowym 25 Odrzucane
Klucz pola / sekcji 64 znaki Odrzucane
Wartość opcji 64 znaki Odrzucane
Treść odpowiedzi 256 KB 413
Etykieta, tekst zastępczy 100 znaków Przycinane
Tekst pomocy 200 znaków Przycinane
Opis sekcji 300 znaków Przycinane
message 200 znaków Przycinane
error 300 znaków Przycinane
Wartość pola z rodziny string 1000 znaków Przycinane
Wartość pola text / static 2000 znaków Przycinane
Kanały lub role w katalogu 500 Obcinane

Odrzucenie jest zgłaszane operatorowi jako „Ta aplikacja wysłała ustawienia, których GameVox nie mógł odczytać: <powód>”, z nazwą przekroczonego limitu i pola, które go przekroczyło. Zduplikowane klucze, nieznany typ, select bez opcji oraz wysłanie jednocześnie sections i fields są odrzucane w ten sam sposób.

Po cichu poprawiane zamiast odrzucane

  • Bieżące value, które nie pasuje do swojego typu, staje się puste. Nieaktualna wartość powinna wyczyścić jedno pole, a nie położyć cały panel.
  • options przy typie, który ich nie używa, są pomijane.
  • min / max / step przy polu nieliczbowym są pomijane.
  • Nierozpoznane wpisy channel_kinds są pomijane.
  • show_if wskazujący nieznany klucz, samo pole lub pole wielowartościowe jest pomijany, a pole wyświetla się bezwarunkowo.

9. Czas i tryby awarii

ZachowanieWartośćCo się dzieje
Okno odpowiedzi 12 s Operator widzi komunikat „Ta aplikacja nie odpowiedziała. Może nie obsługiwać ustawień w aplikacji.”
Czas życia nonce 15 s Spóźniona odpowiedź jest odrzucana, zamiast trafić donikąd. Odpowiedź z wygasłym nonce zwraca 404.
Czas odnowienia 2 s na akcję, na aplikację, na serwer „Daj aplikacji chwilę i spróbuj ponownie.” describe i save mają osobne okna, więc otwarcie panelu nie blokuje natychmiastowego zapisu.
Aplikacja offline - Odmowa od razu: „Ta aplikacja jest offline, więc nie można jej teraz skonfigurować.” Żadne zdarzenie nie jest wysyłane.
Odpowiada niewłaściwa aplikacja - 403. Nonce jest powiązany z aplikacją, dla której został wydany.
Zduplikowana odpowiedź - Wygrywa pierwsza odpowiedź; druga nic nie robi.

Każde żądanie zamienia się w zdarzenie gatewaya wysyłane do twojej aplikacji, więc czas odnowienia istnieje po to, żeby operator nie mógł używać GameVox do zasypywania żądaniami cudzego bota. Zaplanuj odpowiednio swój handler describe: uruchamia się przy otwarciu strony i odczyt z bazy danych jest w porządku, ale dwanaście sekund to twarda granica.

10. Pełny przykład (discord.js)

Gotowy do wklejenia, brakuje tylko twojej warstwy przechowywania danych. To ten sam kształt, którego używa implementacja referencyjna.

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) {
    // Odpowiadaj nawet przy błędzie: ciche porzucenie wygląda jak zepsuta aplikacja.
    await reply(nonce, guildId, {
      error: "Bot nie mógł odczytać swoich ustawień dla tego serwera.",
    }).catch(() => undefined);
  }
}

async function describe(guildId) {
  const cfg = await store.get(guildId);
  return [
    {
      key: "general",
      label: "Ogólne",
      description: "Jak bot zachowuje się na tym serwerze.",
      fields: [
        { key: "prefix", label: "Prefiks komend", type: "string",
          value: cfg.prefix, max_length: 4 },
        { key: "ignored", label: "Ignorowane kanały", type: "channels",
          value: cfg.ignored, channel_kinds: POSTABLE,
          help: "Na tych kanałach komendy nie są obsługiwane." },
      ],
    },
    {
      key: "welcome",
      label: "Wiadomości powitalne",
      description: "Wysyłana, gdy ktoś dołącza.",
      fields: [
        { key: "welcome.enabled", label: "Wyślij wiadomość powitalną",
          type: "boolean", value: cfg.welcome.enabled === true },
        { key: "welcome.channel", label: "Kanał", type: "channel",
          value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
          show_if: { key: "welcome.enabled", equals: true } },
        { key: "welcome.message", label: "Wiadomość", type: "text",
          value: cfg.welcome.message ?? "", max_length: 1800,
          placeholder: "Witaj {user} na {server}!",
          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 "Ustawienia zapisane.";
}

11. Lista kontrolna przed wydaniem

  • Każdy klucz pola jest stały. Zmiana nazwy osieroca wszystko, co operator już skonfigurował.
  • Każdy identyfikator z selektora jest ponownie sprawdzany w gildii przed zapisaniem.
  • Pusty secret jest traktowany jako bez zmian, nigdy jako wyczyszczony.
  • Ścieżka describe odpowiada w czasie wyraźnie krótszym niż 12 sekund, łącznie z odczytem z bazy danych.
  • Każda ścieżka błędu nadal odpowiada: z error, a nie milczeniem.
  • required oraz min/max są ponownie egzekwowane przy zapisie.
  • Pusty selektor oznacza, że katalog był niedostępny, a nie że operator wyczyścił pole.
  • Handler nic nie robi na Discordzie, więc jedna wersja działa na obu platformach.

Kto może otworzyć panel

Właściciel serwera lub członek z uprawnieniem Zarządzaj serwerem: to samo kryterium, które decyduje o instalowaniu i odinstalowywaniu aplikacji. Na serwerze hostowanym samodzielnie sprawdzenie odbywa się na serwerze klienta, więc tamtejszy administrator nie dostanie odmowy z powodu braku wiersza uprawnień w chmurze.

GameVox od razu odrzuca żądanie, jeśli twoja aplikacja nie jest zainstalowana na tym serwerze, więc żądanie panelu może trafić tylko do aplikacji, którą operator już autoryzował.

← Przejście z Discorda  ·  Powrót do dokumentacji