In-App Settings
Kayang i-render ng GameVox ang configuration form ng app mo sa loob ng client. Idine-describe ng app mo ang form sa gateway, ine-edit ito ng operator sa Mga Setting ng Server ▸ Mga Integration ▸ Mga Setting, at diretsong bumabalik sa bot mo ang na-edit na values. Walang ini-store ang GameVox: ang app mo pa rin ang nag-iisang may-ari ng configuration nito.
Walang katapat ito sa Discord. Ito lang ang bahagi ng GameVox app platform na sinadyang hindi compatible, dahil wala namang nasa kabilang side na puwedeng pag-compatible-an.
select, isang boolean at isang channels picker, naka-group sa isang section na may description. Walang ipinadalang channel ids ang bot: ang GameVox ang nag-supply ng listahang iyon. Tinakpan ang mga pangalan ng server at channel.Bakit ito meron
Sa Discord, ang bot na may kahit anong configuration ay may kasamang web dashboard: domain, OAuth2 login, session handling, permission check na binabasa ulit ang guild list ng user, CSRF protection, at hosting para sa lahat ng iyon. Kadalasan, para lang makapili ang operator ng log channel at makapag-toggle ng tatlong features. Alam na ng GameVox kung sino ang operator, aling server ang kino-configure nila, at kung pinapayagan silang i-configure ito, kaya direkta na nitong ibinibigay ang surface na iyon sa app mo.
| Discord | GameVox | |
|---|---|---|
| Saan kino-configure ng operators ang app mo | Sa web dashboard na ikaw ang nagho-host | Mga Setting ng Server ▸ Mga Integration ▸ Mga Setting |
| Ano ang bubuuin mo | Web app, OAuth2 login, sessions, permission checks, hosting | Isang gateway handler at isang REST call |
| Sino ang nag-a-authenticate sa operator | Ikaw, gamit ang OAuth2 | Ang GameVox, bago pa man tanungin ang app mo |
| Sino ang nag-i-store ng configuration | Ikaw | Ikaw. Walang itinatagong kahit ano ang GameVox. |
| Channel at role pickers | Ikaw ang kukuha ng channels at roles ng guild at ikaw ang magre-render | Magbibigay ka ng type; ang GameVox ang magsu-supply ng choices |
| Gastos para mag-expose ng isang toggle | Isang dashboard | Mga dalawampung linya |
Wala itong pinapalitan. Gumagana pa rin ang slash commands, at kung may dashboard ka na, puwede mo itong panatilihin. Hindi exclusive ang dalawa, at maraming apps ang gugustuhin ang panel para sa limang settings na talagang binabago ng operators at ang dashboard para sa lahat ng iba pa.
Paano ito gumagana
Binuksan ng operator ang Settings tab
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ ang bot mo
│
ang bot ─── POST /applications/@me/settings-response ──────────┘
{ nonce, version: 2, sections: [ ... ] }
│
▼
Nire-render ng GameVox ang form, ine-edit ng operator, kini-click ang Save
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ ang bot mo
│
ang bot ─── POST /applications/@me/settings-response ──────────┘
{ nonce, message: "Na-save." } Dalawang action, isang request shape, isang reply endpoint. Tinatanong ng describe kung ano ang ine-expose ng app mo para sa server na ito; ibinabalik naman ng save ang binago ng operator. Parehong paraan ang pagsagot sa dalawa.
1. Pagtanggap ng request
Dini-dispatch ng GameVox ang APP_SETTINGS_REQUEST sa gateway papunta sa kung aling session ang may hawak ng connection ng app mo. Walang intent na nagga-gate dito: naka-address ang dispatch sa app mo mismo, hindi fina-filter laban sa intent mask, kaya darating ito kahit ano pa ang in-identify mo.
{
"op": 0,
"t": "APP_SETTINGS_REQUEST",
"d": {
"nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
"server_id": "1387452901234567890",
"action": "describe",
"schema_version": 2
}
} | Field | Type | Mga tala |
|---|---|---|
nonce | string | Opaque. Ibalik ito nang verbatim; ito lang ang nag-uugnay sa reply mo at sa naghihintay na operator. Puwedeng sagutin sa loob ng 15 segundo. |
server_id | snowflake | Ang guild, sa parehong id space na ginagamit ng bawat ibang dispatch. Ito ang id na dala ng GUILD_CREATE mo, kaya mare-resolve ito ng existing mong guild cache. |
action | string | "describe" o "save". |
schema_version | integer | Ang pinaka-rich na schema na kayang i-render ng server na ito. Sa ngayon, 2. Tingnan ang Pagsisilbi sa mas lumang server. |
values | object | Nasa save lang. Ang keys ay ang field keys na ipinadala mo. |
Ang server_id ay sine-stamp ng GameVox mula sa sariling authenticated na request ng operator. Hindi ito binabasa pabalik mula sa reply mo, kaya hindi puwedeng sagutin ng app ang tanong tungkol sa isang server gamit ang settings para sa ibang server.
Pagbasa ng dispatch sa library mo
Walang bot library na may handler para sa event na wala sa Discord, kaya nadodrop ang packet bago pa ito umabot sa event handler. Bawat library ay nag-e-expose ng raw dispatch stream para mismo sa kasong ito:
| Library | Hook | I-enable |
|---|---|---|
| discord.js (v14) | client.on('raw', packet) | Naka-on by default. Ine-emit para sa bawat dispatch bago i-handle ang packet. |
| discord.py (v2) | on_socket_raw_receive(msg) | Ipasa ang enable_debug_events=True sa client. |
| Eris | client.on('rawWS', packet) | Naka-on by default. |
| JDA | RawGatewayEvent | JDABuilder.setRawEventsEnabled(true) |
| DSharpPlus | DiscordClient.UnknownEvent | Naka-on by default. EventName kasama ang raw na Json. |
| serenity | RawEventHandler::raw_event | ClientBuilder::raw_event_handler |
Sa totoong Discord, hindi kailanman magfa-fire ang handler na ito, kaya tatakbo ang parehong build sa dalawang platform nang walang branch.
2. Pag-reply
Sumagot sa REST, hindi sa gateway. Bawat library ay may HTTP client; karamihan ay hindi pinapayagan ang application code na magsulat ng kahit anong opcode sa socket, kaya ganito ang shape ng reply.
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": "Opsyonal na note na ipinapakita sa itaas ng form.",
"sections": [
{
"key": "welcome",
"label": "Mga welcome message",
"description": "Pino-post kapag may sumali.",
"fields": [
{ "key": "welcome.enabled", "label": "Magpadala ng welcome message",
"type": "boolean", "value": true }
]
}
]
} Nagbabalik ng 204 No Content ang successful na reply. Puwede mong idugtong ang ?server_id= kasama ang guild id; ginagamit lang ito para ma-attribute ang call sa activity log ng app mo.
| Key | Type | Mga tala |
|---|---|---|
nonce | string | Kailangan. Galing sa request. |
version | integer | Ang schema na ginagamit mo sa pagsagot. Ipadala ang 2 kapag nagpapadala ka ng sections. |
sections | array | Schema v2 na form. Hindi puwedeng kasabay ng fields. |
fields | array | Schema v1 na flat form. Nire-render bilang isang section na walang label. |
message | string | Sa describe, isang note sa itaas ng form. Sa save, ang confirmation toast. Pinuputol sa 200 characters. |
error | string | Ipinapakita sa operator sa halip na form. Pinuputol sa 300 characters. |
Laging sumagot, kahit may failure. Kapag tahimik na dinrop ang request, maiiwan ang operator sa spinner hanggang sa 12-second timeout ng GameVox, na mababasa bilang “sira ang app na ito” sa halip na “nagka-problema lang minsan”.
3. Mga field type
Labindalawang type, at sarado ang listahan: ang hindi kilalang type ay nagre-reject sa buong reply sa halip na mag-render bilang wala.
| Type | Control | value na ipinapadala mo | Ano ang babalik sa iyo pag-save |
|---|---|---|---|
string | Isang linyang text box | string | string ("" kapag blangko) |
text | Textarea | string | string |
boolean | Toggle switch | boolean | true / false |
number | Numeric input | number | number, o null kapag kinlear |
select | Dropdown ng options mo | option value | string, o null kapag unset |
multiselect | Checklist ng options mo | array ng option values | array ng strings (puwedeng empty) |
channel | Dropdown ng channels ng server na ito | channel snowflake | snowflake string, o null |
channels | Checklist ng channels ng server na ito | array ng snowflakes | array ng snowflake strings |
role | Dropdown ng mga grupo ng server na ito | role snowflake | snowflake string, o null |
roles | Checklist ng mga grupo ng server na ito | array ng snowflakes | array ng snowflake strings |
color | Colour swatch | #rrggbb | #rrggbb, laging meron |
static | Read-only na linya ng text | string | Hindi kailanman sina-submit: wala ang key sa values |
Laging may value ang colour input, kaya ang app na walang in-store ay makakatanggap ng #000000 sa unang save. Kung kailangan mo ng “walang kulay”, ipares ang swatch sa isang boolean.
4. Mga field attribute
| Attribute | Para sa | Epekto |
|---|---|---|
key | lahat | Kailangan. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Unique sa buong reply, hindi kada section. |
label | lahat | Key ang fallback. Pinuputol sa 100 characters. |
help | lahat | Hint line sa ilalim ng control. Pinuputol sa 200. |
placeholder | text inputs, number | Pinuputol sa 100. |
options | select, multiselect | Kailangan para sa dalawang iyon. { value, label, description }, max 25. Dinodrop sa kahit anong ibang type. |
min , max , step | number | Hinuhubog ang input. Advisory lang: i-check ulit ang value na matatanggap mo. |
max_length | string, text | Nilalagyan ng cap ang input, hanggang sa platform ceiling para sa type na iyon. |
channel_kinds | channel, channels | Pinakikitid ang picker. text, voice, forum, news, fileshare, header. Dinodrop ang hindi kilalang kinds; kapag empty ang resulta, lahat ng kind ang ibig sabihin. |
required | lahat | Minamarkahan ang label at pinapalitan ang “Wala” slot ng picker ng “Pumili…”. Presentational lang: i-enforce ito pag-save. |
disabled | lahat | Ginagawang gray ang control. Sina-submit pa rin, kasama ang kasalukuyang value nito. |
secret | string, text | Mina-mask ang input at binablangko ang kasalukuyang value bago ipadala. Nire-reject ang reply kapag ibang type. |
show_if | lahat | { key, equals }. Nakatago ang field hanggang may ibang field sa parehong reply na may hawak ng value na iyon. |
Equality test lang ang show_if at wala nang iba: walang expressions, walang operators. Ang nire-reference na key ay dapat galing sa isang single-value field sa parehong reply at hindi puwedeng sariling key ng field; ang condition na pumalya sa alinman dito ay dinodrop at palaging lalabas ang field. Puwedeng string, boolean, number o null ang equals.
Ang nakatagong field ay sina-submit pa rin. May value ito na ipinadala ng app mo, at kapag dinrop ito, mababasa ng app mo na parang kinlear ito ng operator.
5. Channel at role pickers
Ito ang bahaging sulit intindihin, dahil binabaligtad nito ang karaniwang ayos. Magbibigay ang app mo ng kind; ang GameVox ang magsu-supply ng choices.
Walang dalang ids ang channel field. Magpapadala ka ng { "type": "channel" } at, kung gusto mo, isang channel_kinds filter. Ikakabit ng GameVox ang sariling channel list ng server na ito, gagawa ang client ng picker mula dito, at ang option lang na ibinigay ng GameVox ang puwedeng i-submit ng operator.
Bawat catalog entry ay naka-key sa parehong snowflake na nakikita na ng app mo para sa channel o grupong iyon. Walang id translation step na puwedeng magkamali: pipili ang operator ng label, makakatanggap ang app mo ng id sa sarili nitong namespace, at ang value na tumutukoy sa bagay na wala sa server na ito ay hindi lang magma-match sa kahit anong option.
{ "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] }. Sa GameVox galing ang listahan, ang kind glyphs at ang ids.| Sitwasyon | Ano ang nakikita ng operator |
|---|---|
| Wala pang naka-store | Isang “Wala” slot, naka-select. Ang required na field ay nagpapakita ng “Pumili…” sa halip, kaya hindi makakapag-commit ang save ng value na walang pumili. |
| Wala na ang naka-store na id (na-delete ang channel) | Pinapanatili bilang “Kasalukuyang napili (wala na sa listahan)”, naka-select pa rin. Hindi ito tahimik na kinli-clear ng pag-save. |
| Mga naka-store na id sa multi-picker na hindi na nakalista | Nire-render at naka-check kasama ng mga live, sa parehong dahilan. |
| Hindi available ang catalog (offline ang self-hosted box) | Naka-disable na picker na may nakasulat na “Hindi available sa ngayon”. Ibinabalik ang naka-store na value pag-save, kaya hindi mabubura ang setting kapag binuksan ang panel habang may outage. |
| Mahigit 500 channels o grupo ang server | Pinuputol ang listahan sa 500 sa halip na magpadala ng isang megabyte ng options. |
Dalawang bagay ang hindi kasama sa role catalog: ang server owner group, na hindi dapat ialok sa kahit anong app bilang assignable role, at ang per-app groups na may dalang sariling permissions ng mga naka-install na app.
6. Mga scenario
Scenario 1: Ang pinakamaliit na useful na panel
Isang toggle, walang sections. Valid ang flat na fields array sa kahit anong schema version at nire-render bilang iisang grupo na walang label.
{
"nonce": nonce,
"fields": [
{ "key": "greetings", "label": "Batiin ang mga bagong miyembro", "type": "boolean", "value": true }
]
} Scenario 2: Pag-group sa sections
{
"nonce": nonce,
"version": 2,
"sections": [
{
"key": "general",
"label": "Pangkalahatan",
"description": "Paano kumikilos ang bot sa buong server na ito.",
"fields": [
{ "key": "prefix", "label": "Command prefix", "type": "string",
"value": "!", "max_length": 4, "help": "Ginagamit para sa legacy na text commands." }
]
},
{
"key": "logging",
"label": "Audit logging",
"fields": [
{ "key": "log.enabled", "label": "I-log ang moderation actions", "type": "boolean", "value": false }
]
}
]
} Opsyonal ang section key pero dapat unique kapag meron. Parehong opsyonal ang label at description; hindi nire-render ang section na walang fields.
Scenario 3: Humingi ng channel
Walang ids, walang pagkuha ng channel list ng guild. Limitahan ang picker sa mga kind na puwedeng paglagyan ng message.
{
"key": "log.channel",
"label": "Log channel",
"type": "channel",
"value": stored.logChannelId ?? null,
"channel_kinds": ["text", "news"],
"help": "Kung saan nire-record ang moderation actions."
} Gamitin ang "channel_kinds": ["header"] kapag category ang gusto mo at hindi lugar na pagpo-post-an: headers ang categories ng GameVox.
Scenario 4: Magpakita ng fields depende sa condition
Ang karaniwang shape: isang boolean na nagga-gate sa natitirang bahagi ng feature nito.
"fields": [
{ "key": "welcome.enabled", "label": "Magpadala ng welcome message",
"type": "boolean", "value": true },
{ "key": "welcome.channel", "label": "Channel", "type": "channel",
"value": stored.welcomeChannel ?? null,
"channel_kinds": ["text", "news"],
"show_if": { "key": "welcome.enabled", "equals": true } },
{ "key": "welcome.message", "label": "Mensahe", "type": "text",
"value": stored.welcomeMessage ?? "",
"max_length": 1800,
"placeholder": "Welcome {user} sa {server}!",
"help": "Pinapalitan ang {user}, {server} at {membercount} kapag ipinadala.",
"show_if": { "key": "welcome.enabled", "equals": true } }
] Puwede ring mag-key ang conditions sa isang select, at ganyan ka gagawa ng mode switch: "show_if": { "key": "mode", "equals": "advanced" }.
Scenario 5: Sarili mong choices
Ang select at multiselect ang dalawang type na may sariling options. Parehong kailangan ng kahit isang option at hanggang 25 lang.
{
"key": "automod.action",
"label": "Kapag nag-match ang isang filter",
"type": "select",
"value": stored.action,
"required": true,
"options": [
{ "value": "delete", "label": "I-delete ang message" },
{ "value": "warn", "label": "Bigyan ng warning ang author", "description": "Dine-delete at dine-DM ang member." },
{ "value": "timeout", "label": "I-timeout ang author", "description": "10 minuto." },
{ "value": "none", "label": "Walang gagawin" }
]
} {
"key": "automod.filters",
"label": "Mga aktibong filter",
"type": "multiselect",
"value": stored.filters,
"options": [
{ "value": "invites", "label": "Discord/GameVox invites" },
{ "value": "links", "label": "Mga link" },
{ "value": "mentions", "label": "Mass mentions" },
{ "value": "caps", "label": "Sobrang caps" }
]
} Lumalabas ang description ng isang option pagkatapos ng label sa dropdown at bilang tooltip sa checklist.
Scenario 6: Roles
Parehong catalog rule gaya ng channels. Inililista ng GameVox ang mga grupo ng server na ito ayon sa rank, kasama ang mga kulay nila.
"fields": [
{ "key": "autorole.role", "label": "Role na ibinibigay pagsali",
"type": "role", "value": stored.autoRole ?? null },
{ "key": "moderator.roles", "label": "Mga role na puwedeng gumamit ng moderation commands",
"type": "roles", "value": stored.modRoles,
"help": "Ang members na may alinman dito ay puwedeng magpatakbo ng /ban at /timeout." }
] Scenario 7: Numbers na may hangganan
{
"key": "automod.threshold",
"label": "Bilang ng messages bago ideklarang raid",
"type": "number",
"value": stored.threshold ?? 10,
"min": 3,
"max": 100,
"step": 1,
"help": "Binibilang sa rolling na 60-second window."
} Hinuhubog lang ng min, max at step ang control. Hindi validator na kontrolado mo ang browser ng operator, kaya i-clamp ulit ang value pagbalik nito. Tandaan din na kapag kinlear ang box, null ang sina-submit, hindi 0.
Scenario 8: Secrets
Markahan ang isang credential bilang secret at bablangkuhin ng GameVox ang kasalukuyang value nito bago ipadala. Wala ito sa WebSocket frame, wala sa DOM, at wala sa screenshot ng panel: empty na masked box na may placeholder na “Walang pagbabago” ang nakikita ng operator.
{
"key": "integrations.apiKey",
"label": "Weather API key",
"type": "string",
"secret": true,
"help": "Iwanang blangko para panatilihin ang kasalukuyang key."
} Ang kontratang sumusunod dito: ang empty na sina-submit na secret ay nangangahulugang “huwag galawin”, hindi kailanman “i-clear ito”. Kung kailangang mag-alis ng operator ng credential, bigyan sila ng explicit na boolean para gawin iyon. Nire-reject ang reply kapag may secret sa kahit anong type maliban sa string o text.
Scenario 9: Read-only na status
Nagre-render ang static ng isang linya ng text at hindi kailanman sina-submit. Gamitin ito para sa state na kailangan ng operator habang nagko-configure pero hindi puwedeng i-edit dito.
"fields": [
{ "key": "plan", "label": "Plan", "type": "static", "value": "Pro: 4,000 lookups/araw" },
{ "key": "usage", "label": "Nagamit ngayong araw", "type": "static", "value": "1,284 lookups" },
{ "key": "lastSync", "label": "Huling sync", "type": "static", "value": "2026-09-02 14:31 UTC" }
] Scenario 10: Pag-handle ng save
May dalang values ang save request, naka-key sa field keys na ipinadala mo. I-persist, tapos sumagot ng confirmation.
// action === "save"
const v = req.values ?? {};
const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Wala na ang bot na ito sa server na iyon." });
const patch = {};
// boolean: laging totoong boolean
patch.welcomeEnabled = v["welcome.enabled"] === true;
// channel: snowflake string o null; i-check ulit laban sa guild na ITO
const chan = v["welcome.channel"];
patch.welcomeChannel =
(typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;
// number: number o null kapag kinlear ng operator ang box
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;
// secret: kapag empty, walang binago
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: "Na-save ang settings." }); Ang message mo ang magiging confirmation toast ng operator. Iniiwan ang form nang eksaktong gaya ng pagkakaiwan nila, kaya puwede pa silang magpatuloy sa pag-edit.
Scenario 11: Pag-reject ng save
Magbalik ng error at ang text mo ang makikita ng operator sa halip na confirmation. Gamitin ito para sa kahit anong hindi mo matatanggap: value na pumalya sa sarili mong validation, plan limit, upstream credential na hindi na gumagana.
// presentational lang ang `required`: dito ito i-enforce.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
return reply(nonce, guildId, {
error: "Piliin kung ano ang gagawin ng AutoMod kapag nag-match ang isang filter.",
});
}
// Anumang bagay na sa side mo lang malalaman.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
return reply(nonce, guildId, {
error: "Tinanggihan ng weather provider ang API key na iyan.",
});
} Ganoon din sa describe: kung down ang database mo, sumagot ng error sa halip na hindi sumagot. Mas maganda para sa operator ang “Hindi mabasa ng app na ito ang settings nito sa ngayon” kaysa sa labindalawang segundong spinner.
Scenario 12: Pagsisilbi sa mas lumang server
May dalang schema_version ang request, kaya hindi mo na kailangang mag-probe. Ibigay ang pinaka-rich na form na talagang kayang i-render ng server.
const version = typeof req.schema_version === "number" ? req.schema_version : 1;
if (version >= 2) {
await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
// v1: flat na listahan, apat na type: string, boolean, number, select
await reply(nonce, { fields: describeV1(guildId) });
} Totoo rin ang kabaligtaran. Nare-render pa rin ang v2 reply na umabot sa mas lumang client: nagpapadala ang GameVox ng flattened na fields array kasabay ng sections, at ang client na mas nauna sa mga bagong type ay dino-drawing ang mga ito bilang text boxes. Degraded, pero walang nakatago.
Scenario 13: Self-hosted servers
Gumagana nang walang binabago. Sa isang self-hosted server, kinukuha ang channel at role catalogs mula sa sariling box ng customer sa halip na sa cloud tables, at hindi ito nakikita ng app mo: parehong field types, parehong snowflakes, parehong reply.
Ang tanging pagkakaibang mapapansin mo: kung hindi ma-reach ang box, empty na darating ang pickers at magre-render bilang “Hindi available sa ngayon”. Ibinabalik pag-save ang naka-store na values ng app mo, kaya walang nawawala. Huwag ituring na “kinlear ng operator” ang empty na picker.
7. Ano ang vina-validate ng GameVox, at ano ang hindi
Chine-check ng GameVox ang shape ng isang save, at sadyang hindi ang kahulugan nito. Hindi nito itinatabi ang form na ni-render nito (ang hindi pag-store ng kahit anong configuration mo ang buong punto), kaya pagdating ng save, walang nakakaalam sa side namin kung aling key ang channel picker at alin ang free-text box.
| Ginagarantiya ng GameVox | Kailangan mo pa ring i-check |
|---|---|
Tugma ang keys sa key charset at hindi __proto__, constructor o prototype | Na ang key ay talagang ipinadala mo |
Ang values ay null, boolean, number, string, o array ng strings: hindi kailanman nested object | Na tugma ang type sa field na dineklara mo |
| May hangganan ang strings (2,000 characters) at hanggang 25 entries lang ang arrays | Ang sarili mong mas mahigpit na limits |
| Bawat picker id ay ibinigay ng GameVox mula sa catalog ng server na ito | Na nare-resolve pa rin ang id sa guild na ito: snapshot lang ang catalogs, at puwedeng ma-delete ang channel sa pagitan ng render at save |
| Ang operator ay server owner o may Pamahalaan ang Server, at naka-install ang app dito | Anumang sarili mong authorisation (plan tier, naka-link na account) |
Hindi ito butas na dala ng panel: kaya na ng operator na mag-POST ng kahit ano sa sariling dashboard ng isang app. Ito ang kontrata, at ito mismo ang dahilan kung bakit ang picker types ay nagbibigay sa iyo ng ids na mini-mint ng GameVox sa halip na hayaan ang app na magdesisyon kung ano ang ibig sabihin ng isang id.
8. Mga limit
Dalawang behavior dito, at mahalaga ang pagkakaiba: nire-reject ng counts at structure ang buong reply, kaya malalaman mo; tahimik namang pinuputol ang text ceilings, kaya hindi kailanman makakakita ang operator ng sirang layout.
| Limit | Value | Kapag lumampas |
|---|---|---|
| Sections kada reply | 12 | Nire-reject |
| Fields kada reply (kabuuan, hindi kada section) | 60 | Nire-reject |
Options kada select / multiselect | 1–25 | Nire-reject |
| Mga naka-select na entry sa multi-value | 25 | Nire-reject |
| Field / section key | 64 chars | Nire-reject |
| Option value | 64 chars | Nire-reject |
| Reply body | 256 KB | 413 |
| Label, placeholder | 100 chars | Pinuputol |
| Help text | 200 chars | Pinuputol |
| Section description | 300 chars | Pinuputol |
message | 200 chars | Pinuputol |
error | 300 chars | Pinuputol |
Value ng field na string-family | 1,000 chars | Pinuputol |
Value ng text / static field | 2,000 chars | Pinuputol |
| Channels o roles sa isang catalog | 500 | Tina-truncate |
Ire-report sa operator ang rejection bilang “Nagpadala ang app na ito ng settings na hindi mabasa ng GameVox: <dahilan>”, kasama ang ceiling na naabot mo at ang field na umabot dito. Ganoon din nire-reject ang duplicate keys, hindi kilalang type, select na walang options, at pagpapadala ng parehong sections at fields.
Tahimik na inaayos sa halip na i-reject
- Nagiging empty ang kasalukuyang
valuena hindi tugma sa type nito. Dapat isang field lang ang mablangko ng stale na value, hindi ang buong panel ang bumagsak. - Dinodrop ang
optionssa type na hindi gumagamit nito. - Dinodrop ang
min/max/stepsa non-numeric na field. - Dinodrop ang hindi kilalang
channel_kindsentries. - Dinodrop ang
show_ifna tumutukoy sa hindi kilalang key, sa sarili nito, o sa multi-value field, at palaging lalabas ang field.
9. Timing at failure modes
| Behavior | Value | Ano ang nangyayari |
|---|---|---|
| Reply window | 12s | Sasabihan ang operator ng “Hindi tumugon ang app na ito. Maaaring hindi nito sinusuportahan ang mga in-app na setting.” |
| Nonce lifetime | 15s | Dinodrop ang late na reply sa halip na i-deliver sa walang tatanggap. Nagbabalik ng 404 ang pagsagot gamit ang expired na nonce. |
| Cooldown | 2s kada action, kada app, kada server | “Bigyan muna ng sandali ang app, tapos subukan ulit.” Magkahiwalay ang windows ng describe at save, kaya hindi nabablock ng pagbukas ng panel ang agarang save. |
| Offline ang app | - | Tinatanggihan agad: “Offline ang app na ito, kaya hindi ito ma-configure sa ngayon.” Walang dispatch na susubukan. |
| Maling app ang sumagot | - | 403. Naka-bind ang nonce sa application kung saan ito ibinigay. |
| Duplicate na reply | - | Ang unang sagot ang panalo; no-op ang pangalawa. |
Bawat request ay nagiging gateway dispatch sa app mo, kaya nandiyan ang cooldown para hindi magamit ng operator ang GameVox para i-spam ang bot ng ibang tao. I-budget nang tama ang describe handler mo: tumatakbo ito pagbukas ng page, at inaasahan ang database read, pero labindalawang segundo ang hangganan.
10. Kumpletong halimbawa (discord.js)
Drop-in na ito, maliban sa sarili mong storage layer. Ito rin ang shape na gamit ng reference implementation.
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) {
// Sumagot kahit may failure: ang tahimik na drop ay mukhang sirang app.
await reply(nonce, guildId, {
error: "Hindi mabasa ng bot ang settings nito para sa server na ito.",
}).catch(() => undefined);
}
}
async function describe(guildId) {
const cfg = await store.get(guildId);
return [
{
key: "general",
label: "Pangkalahatan",
description: "Paano kumikilos ang bot sa buong server na ito.",
fields: [
{ key: "prefix", label: "Command prefix", type: "string",
value: cfg.prefix, max_length: 4 },
{ key: "ignored", label: "Mga ignored na channel", type: "channels",
value: cfg.ignored, channel_kinds: POSTABLE,
help: "Hindi sinasagot ang commands sa mga channel na ito." },
],
},
{
key: "welcome",
label: "Mga welcome message",
description: "Pino-post kapag may sumali.",
fields: [
{ key: "welcome.enabled", label: "Magpadala ng welcome message",
type: "boolean", value: cfg.welcome.enabled === true },
{ key: "welcome.channel", label: "Channel", type: "channel",
value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
show_if: { key: "welcome.enabled", equals: true } },
{ key: "welcome.message", label: "Mensahe", type: "text",
value: cfg.welcome.message ?? "", max_length: 1800,
placeholder: "Welcome {user} sa {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 "Na-save ang settings.";
} 11. Checklist bago mag-ship
- Stable ang bawat field key. Kapag ni-rename ang isa, mao-orphan ang anumang na-configure na ng operator.
- Bawat id mula sa picker ay chine-check ulit laban sa guild bago isulat.
- Ang empty na
secretay itinuturing na walang binago, hindi kailanman na-clear. - Sumasagot ang
describepath nang malayong mas mabilis sa 12 segundo, kasama ang database read nito. - Sumasagot pa rin ang bawat failure path: gamit ang
error, hindi ang katahimikan. - Ine-enforce ulit pag-save ang
requiredatmin/max. - Ang empty na picker ay nangangahulugang hindi available ang catalog, hindi na kinlear ng operator ang field.
- No-op ang handler sa Discord, kaya iisang build ang tatakbo sa dalawa.
Sino ang puwedeng magbukas ng panel
Ang server owner, o member na may Pamahalaan ang Server: parehong gate na namamahala sa pag-install at pag-uninstall ng apps. Sa self-hosted server, sa sariling box ng customer ginagawa ang check, kaya hindi tatanggihan ang admin doon dahil lang walang cloud permission row.
Tahasang tinatanggihan ng GameVox ang request kung hindi naka-install ang app mo sa server na iyon, kaya ang panel request ay makakarating lang sa app na na-authorize na ng operator.