Uygulama İçi Ayarlar
GameVox, uygulamanın yapılandırma formunu istemcinin içinde gösterebilir. Uygulaman formu gateway üzerinden tanımlar, bir yönetici bunu Sunucu Ayarları ▸ Entegrasyonlar ▸ Ayarlar içinde düzenler ve düzenlenen değerler doğrudan botuna geri gelir. GameVox bunların hiçbirini saklamaz: yapılandırmanın tek sahibi uygulaman olarak kalır.
Bunun Discord'da karşılığı yok. GameVox uygulama platformunun bilerek uyumlu yapılmayan tek parçası, çünkü öbür tarafta uyumlu olunacak bir şey yok.
select, bir boolean ve bir channels seçicisi. Bot hiçbir kanal ID'si göndermedi: o listeyi GameVox sağladı. Sunucu ve kanal adları gizlendi.Bu neden var
Discord'da herhangi bir yapılandırması olan bir bot, bir web paneliyle gelir: bir alan adı, bir OAuth2 girişi, oturum yönetimi, kullanıcının guild listesini yeniden okuyan bir izin kontrolü, CSRF koruması ve bunların hepsi için barındırma. Çoğu zaman da tüm bunlar, bir yöneticinin bir log kanalı seçip üç özelliği açıp kapatabilmesi için. GameVox, yöneticinin kim olduğunu, hangi sunucuyu yapılandırdığını ve bunu yapılandırmaya yetkisi olup olmadığını zaten bildiği için bu yüzeyi doğrudan uygulamana sunar.
| Discord | GameVox | |
|---|---|---|
| Yöneticiler uygulamanı nerede yapılandırır | Senin barındırdığın bir web panelinde | Sunucu Ayarları ▸ Entegrasyonlar ▸ Ayarlar |
| Senin geliştirdiğin | Web uygulaması, OAuth2 girişi, oturumlar, izin kontrolleri, barındırma | Bir gateway handler'ı ve bir REST çağrısı |
| Yöneticinin kimliğini kim doğrular | Sen, OAuth2 ile | GameVox, uygulamana daha soru bile gelmeden |
| Yapılandırmayı kim saklar | Sen | Sen. GameVox hiçbir şey tutmaz. |
| Kanal ve rol seçicileri | Guild'in kanallarını ve rollerini çekip kendin gösterirsin | Sen bir tür belirtirsin; seçenekleri GameVox sağlar |
| Tek bir anahtarı sunmanın maliyeti | Bir panel | Yaklaşık yirmi satır |
Bu hiçbir şeyin yerini almıyor. Slash komutları yine çalışır ve zaten bir panelin varsa onu tutabilirsin: ikisi birbirini dışlamaz. Pek çok uygulama, yöneticilerin gerçekten değiştirdiği beş ayar için paneli, diğer her şey için web panelini isteyecektir.
Nasıl çalışır
Yönetici Ayarlar sekmesini açar
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "describe") ────▶ botun
│
botun ─── POST /applications/@me/settings-response ──────────┘
{ nonce, version: 2, sections: [ ... ] }
│
▼
GameVox formu gösterir, yönetici düzenler ve Kaydet'e tıklar
│
▼
GameVox ──── APP_SETTINGS_REQUEST (action: "save", values) ─▶ botun
│
botun ─── POST /applications/@me/settings-response ──────────┘
{ nonce, message: "Kaydedildi." } İki eylem, tek bir istek biçimi, tek bir yanıt endpoint'i. describe, uygulamanın bu sunucu için neyi sunduğunu sorar; save, yöneticinin değiştirdiklerini geri iletir. İkisi de aynı şekilde yanıtlanır.
1. İsteği alma
GameVox, APP_SETTINGS_REQUEST olayını gateway üzerinden uygulamanın bağlantısını tutan oturuma gönderir. Hiçbir intent bunu kısıtlamaz: bu mesaj bir intent maskesine göre filtrelenmez, doğrudan uygulamana adreslenir; yani identify sırasında ne gönderdiysen gönder, yine gelir.
{
"op": 0,
"t": "APP_SETTINGS_REQUEST",
"d": {
"nonce": "5f3c1b0e-8a4d-4b2f-9c11-2f7a6d0b4e93",
"server_id": "1387452901234567890",
"action": "describe",
"schema_version": 2
}
} | Alan | Tür | Notlar |
|---|---|---|
nonce | string | Opak. Olduğu gibi geri gönder; yanıtını bekleyen yöneticiye bağlayan tek şey budur. 15 saniye boyunca yanıtlanabilir. |
server_id | snowflake | Guild; diğer tüm mesajların kullandığı aynı ID alanında. Bu, GUILD_CREATE olayında gelen ID'dir, yani mevcut guild önbelleğin bunu çözümler. |
action | string | "describe" veya "save". |
schema_version | integer | Bu sunucunun gösterebileceği en zengin şema. Şu anda 2. Bkz. Eski bir sunucuya hizmet verme. |
values | object | Yalnızca save isteğinde bulunur. Anahtarlar, gönderdiğin alan anahtarlarıdır. |
server_id, GameVox tarafından yöneticinin kendi kimliği doğrulanmış isteğinden damgalanır. Asla yanıtından geri okunmaz; böylece bir uygulama, bir sunucuyla ilgili bir soruyu başka bir sunucunun ayarlarıyla yanıtlayamaz.
Mesajı kütüphanende okuma
Hiçbir bot kütüphanesi Discord'da olmayan bir olay için handler içermez, bu yüzden paket bir olay handler'ına ulaşmadan atılır. Her kütüphane tam da bu durum için ham mesaj akışını sunar:
| Kütüphane | Hook | Etkinleştirme |
|---|---|---|
| discord.js (v14) | client.on('raw', packet) | Varsayılan olarak açık. Her mesaj için, paket işlenmeden önce yayınlanır. |
| discord.py (v2) | on_socket_raw_receive(msg) | İstemciye enable_debug_events=True geçir. |
| Eris | client.on('rawWS', packet) | Varsayılan olarak açık. |
| JDA | RawGatewayEvent | JDABuilder.setRawEventsEnabled(true) |
| DSharpPlus | DiscordClient.UnknownEvent | Varsayılan olarak açık. EventName ve ham Json. |
| serenity | RawEventHandler::raw_event | ClientBuilder::raw_event_handler |
Gerçek Discord'a karşı bu handler hiç tetiklenmez, yani aynı build hiçbir dallanma olmadan iki platformda da çalışır.
2. Yanıt verme
Gateway üzerinden değil, REST üzerinden yanıt ver. Her kütüphane bir HTTP istemcisi sunar; çoğu, uygulama kodunun sokete rastgele opcode yazmasına izin vermez. Yanıtın bu şekilde tasarlanmasının nedeni budur.
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": "Formun üstünde gösterilen isteğe bağlı not.",
"sections": [
{
"key": "welcome",
"label": "Karşılama mesajları",
"description": "Biri katıldığında gönderilir.",
"fields": [
{ "key": "welcome.enabled", "label": "Karşılama mesajı gönder",
"type": "boolean", "value": true }
]
}
]
} Başarılı bir yanıt 204 No Content döndürür. Guild ID'siyle birlikte ?server_id= ekleyebilirsin; bu yalnızca çağrıyı uygulamanın etkinlik kaydında ilişkilendirmek için kullanılır.
| Anahtar | Tür | Notlar |
|---|---|---|
nonce | string | Zorunlu. İstekten gelir. |
version | integer | Yanıt verdiğin şema. sections gönderiyorsan 2 gönder. |
sections | array | Şema v2 formu. fields ile birlikte kullanılamaz. |
fields | array | Şema v1 düz formu. Etiketsiz tek bir bölüm olarak gösterilir. |
message | string | describe sırasında formun üstündeki not. save sırasında onay bildirimi (toast). 200 karakterde kırpılır. |
error | string | Yöneticiye form yerine gösterilir. 300 karakterde kırpılır. |
Hata durumunda da dahil, her zaman yanıt ver. İsteği sessizce düşürmek, yöneticiyi GameVox'un 12 saniyelik zaman aşımına kadar dönen bir yükleme simgesinin başında bırakır; bu da “bir kere bir şey ters gitti” yerine “bu uygulama bozuk” diye algılanır.
3. Alan türleri
On iki tür var ve liste kapalı: tanınmayan bir type, hiçbir şey göstermek yerine yanıtın tamamını reddeder.
| Tür | Kontrol | Gönderdiğin value | Kaydetmede geri aldığın |
|---|---|---|---|
string | Tek satırlık metin kutusu | string | string (boşsa "") |
text | Metin alanı | string | string |
boolean | Açma/kapama anahtarı | boolean | true / false |
number | Sayı girişi | number | number, temizlendiyse null |
select | Kendi options değerlerinden açılır liste | seçenek değeri | string, ayarlanmadıysa null |
multiselect | Kendi options değerlerinden onay listesi | seçenek değerleri dizisi | string dizisi (boş olabilir) |
channel | Bu sunucunun kanallarından açılır liste | kanal snowflake'i | snowflake string'i veya null |
channels | Bu sunucunun kanallarından onay listesi | snowflake dizisi | snowflake string'leri dizisi |
role | Bu sunucunun gruplarından açılır liste | rol snowflake'i | snowflake string'i veya null |
roles | Bu sunucunun gruplarından onay listesi | snowflake dizisi | snowflake string'leri dizisi |
color | Renk örneği | #rrggbb | #rrggbb, her zaman var |
static | Salt okunur metin satırı | string | Asla gönderilmez: anahtar values içinde yer almaz |
Bir renk girişi her zaman bir değer tutar, bu yüzden hiçbir şey saklamamış bir uygulama ilk kaydetmede #000000 alır. “Renk yok” seçeneğine ihtiyacın varsa renk örneğini bir boolean ile eşleştir.
4. Alan öznitelikleri
| Öznitelik | Uygulandığı | Etkisi |
|---|---|---|
key | tümü | Zorunlu. [A-Za-z0-9][A-Za-z0-9._:-]{0,63}. Bölüm başına değil, yanıtın tamamında benzersiz. |
label | tümü | Yoksa anahtar kullanılır. 100 karakterde kırpılır. |
help | tümü | Kontrolün altındaki ipucu satırı. 200'de kırpılır. |
placeholder | metin girişleri, number | 100'de kırpılır. |
options | select, multiselect | Bu ikisi için zorunlu. { value, label, description }, en fazla 25. Diğer türlerde atılır. |
min , max , step | number | Girişi şekillendirir. Tavsiye niteliğinde: aldığın değeri yeniden kontrol et. |
max_length | string, text | Girişi o türün platform tavanına kadar sınırlar. |
channel_kinds | channel, channels | Seçiciyi daraltır. text, voice, forum, news, fileshare, header. Bilinmeyen türler atılır; boş bir sonuç tüm türler demektir. |
required | tümü | Etiketi işaretler ve seçicinin “Yok” seçeneğini “Seç…” ile değiştirir. Yalnızca görsel: kaydetme sırasında kendin uygula. |
disabled | tümü | Kontrolü soluklaştırır. Mevcut değeriyle yine gönderilir. |
secret | string, text | Girişi maskeler ve giden verideki mevcut değeri boşaltır. Başka herhangi bir tür yanıtı reddettirir. |
show_if | tümü | { key, equals }. Aynı yanıttaki başka bir alan o değeri taşıyana kadar alanı gizler. |
show_if yalnızca bir eşitlik testidir, başka hiçbir şey değil: ifade yok, operatör yok. Başvurulan anahtar aynı yanıttaki tek değerli bir alana ait olmalı ve alanın kendi anahtarı olmamalıdır; bunlardan herhangi birini sağlamayan bir koşul atılır ve alan koşulsuz gösterilir. equals bir string, boolean, number veya null olabilir.
Gizli bir alan yine de gönderilir. Uygulamanın gönderdiği bir değeri vardır ve onu atlamak, uygulamana yöneticinin onu temizlemiş olduğu şeklinde yansırdı.
5. Kanal ve rol seçicileri
Anlaşılmaya değer kısım bunlar, çünkü alışılmış düzeni tersine çeviriyorlar. Uygulaman bir tür belirtir; seçenekleri GameVox sağlar.
Bir kanal alanı hiçbir ID taşımaz. { "type": "channel" } ve istersen bir channel_kinds filtresi gönderirsin. GameVox bu sunucunun kendi kanal listesini ekler, istemci bundan bir seçici oluşturur ve yönetici yalnızca GameVox'un verdiği bir seçeneği gönderebilir.
Her katalog girdisi, o kanal veya grup için uygulamanın zaten gördüğü aynı snowflake ile anahtarlanır. Yanlış yapılabilecek bir ID çevirme adımı yok: yönetici bir etiket seçer, uygulaman kendi ad alanında bir ID alır ve bu sunucunun dışındaki bir şeyi gösteren bir değer hiçbir seçenekle eşleşmez.
{ "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] } idi. Liste, tür simgeleri ve ID'ler GameVox'tan geliyor.| Durum | Yöneticinin gördüğü |
|---|---|
| Henüz hiçbir şey saklanmamış | Seçili bir “Yok” seçeneği. required bir alan bunun yerine “Seç…” gösterir; böylece kaydetme, kimsenin seçmediği bir değeri kaydedemez. |
| Saklanan ID artık yok (kanal silinmiş) | “Mevcut seçim (artık listelenmiyor)” olarak tutulur, hâlâ seçilidir. Kaydetmek onu sessizce temizlemez. |
| Çoklu seçicide artık listelenmeyen saklı ID'ler | Aynı nedenle, canlı olanların yanında gösterilir ve işaretli kalır. |
| Katalog kullanılamıyor (kendi barındırılan sunucu çevrimdışı) | “Şu anda kullanılamıyor” yazan devre dışı bir seçici. Saklanan değer kaydetmede geri gönderilir, böylece bir kesinti sırasında paneli açmak bir ayarı silemez. |
| Sunucuda 500'den fazla kanal veya grup var | Bir megabaytlık seçenek göndermek yerine liste 500'de kesilir. |
Rol kataloğu iki şeyi hariç tutar: hiçbir uygulamaya atanabilir bir rol olarak sunulmaması gereken sunucu sahibi grubu ve kurulu uygulamaların kendi izinlerini taşıyan uygulama başına gruplar.
6. Senaryolar
Senaryo 1: En küçük işe yarar panel
Tek bir anahtar, bölüm yok. Düz bir fields dizisi her şema sürümünde geçerlidir ve etiketsiz tek bir grup olarak gösterilir.
{
"nonce": nonce,
"fields": [
{ "key": "greetings", "label": "Yeni üyeleri karşıla", "type": "boolean", "value": true }
]
} Senaryo 2: Bölümlere ayırma
{
"nonce": nonce,
"version": 2,
"sections": [
{
"key": "general",
"label": "Genel",
"description": "Botun bu sunucu genelinde nasıl davrandığı.",
"fields": [
{ "key": "prefix", "label": "Komut öneki", "type": "string",
"value": "!", "max_length": 4, "help": "Eski tip metin komutları için kullanılır." }
]
},
{
"key": "logging",
"label": "Denetim kaydı",
"fields": [
{ "key": "log.enabled", "label": "Moderasyon işlemlerini kaydet", "type": "boolean", "value": false }
]
}
]
} Bölüm key değeri isteğe bağlıdır ama varsa benzersiz olmalıdır. label ve description ikisi de isteğe bağlıdır; hiç alanı olmayan bir bölüm gösterilmez.
Senaryo 3: Bir kanal iste
ID yok, guild'in kanal listesini çekmek yok. Seçiciyi mesaj tutabilen türlerle sınırla.
{
"key": "log.channel",
"label": "Log kanalı",
"type": "channel",
"value": stored.logChannelId ?? null,
"channel_kinds": ["text", "news"],
"help": "Moderasyon işlemlerinin kaydedildiği yer."
} Gönderi yapılacak bir yer yerine bir kategori istiyorsan "channel_kinds": ["header"] kullan: header'lar GameVox'un kategorileridir.
Senaryo 4: Alanları koşullu gösterme
Alışılmış yapı: özelliğin geri kalanını açıp kapatan bir boolean.
"fields": [
{ "key": "welcome.enabled", "label": "Karşılama mesajı gönder",
"type": "boolean", "value": true },
{ "key": "welcome.channel", "label": "Kanal", "type": "channel",
"value": stored.welcomeChannel ?? null,
"channel_kinds": ["text", "news"],
"show_if": { "key": "welcome.enabled", "equals": true } },
{ "key": "welcome.message", "label": "Mesaj", "type": "text",
"value": stored.welcomeMessage ?? "",
"max_length": 1800,
"placeholder": "{server} sunucusuna hoş geldin {user}!",
"help": "{user}, {server} ve {membercount} gönderilirken yerlerine konur.",
"show_if": { "key": "welcome.enabled", "equals": true } }
] Koşullar bir select alanına da bağlanabilir; bir mod anahtarını böyle kurarsın: "show_if": { "key": "mode", "equals": "advanced" }.
Senaryo 5: Kendi seçeneklerin
select ve multiselect, kendi seçeneklerini taşıyan iki türdür. İkisi de en az bir seçenek gerektirir ve en fazla 25'e izin verir.
{
"key": "automod.action",
"label": "Bir filtre eşleştiğinde",
"type": "select",
"value": stored.action,
"required": true,
"options": [
{ "value": "delete", "label": "Mesajı sil" },
{ "value": "warn", "label": "Yazarı uyar", "description": "Siler ve üyeye DM gönderir." },
{ "value": "timeout", "label": "Yazara zaman aşımı ver", "description": "10 dakika." },
{ "value": "none", "label": "Hiçbir şey yapma" }
]
} {
"key": "automod.filters",
"label": "Etkin filtreler",
"type": "multiselect",
"value": stored.filters,
"options": [
{ "value": "invites", "label": "Discord/GameVox davetleri" },
{ "value": "links", "label": "Bağlantılar" },
{ "value": "mentions", "label": "Toplu etiketlemeler" },
{ "value": "caps", "label": "Aşırı büyük harf" }
]
} Bir seçeneğin description değeri, açılır listede etiketin arkasında, onay listesinde ise ipucu balonu olarak görünür.
Senaryo 6: Roller
Kanallarla aynı katalog kuralı. GameVox bu sunucunun gruplarını renkleriyle birlikte rütbeye göre listeler.
"fields": [
{ "key": "autorole.role", "label": "Katılınca verilen rol",
"type": "role", "value": stored.autoRole ?? null },
{ "key": "moderator.roles", "label": "Moderasyon komutlarını kullanabilecek roller",
"type": "roles", "value": stored.modRoles,
"help": "Bunlardan herhangi birine sahip üyeler /ban ve /timeout çalıştırabilir." }
] Senaryo 7: Sınırlı sayılar
{
"key": "automod.threshold",
"label": "Baskın ilan edilmeden önceki mesaj sayısı",
"type": "number",
"value": stored.threshold ?? 10,
"min": 3,
"max": 100,
"step": 1,
"help": "Kayan 60 saniyelik bir pencerede sayılır."
} min, max ve step yalnızca kontrolü şekillendirir. Yöneticinin tarayıcısı senin kontrol ettiğin bir doğrulayıcı değil, bu yüzden değer geri geldiğinde onu yeniden sınırla. Ayrıca kutuyu temizlemenin 0 değil null gönderdiğini unutma.
Senaryo 8: Secret'lar
Bir kimlik bilgisini secret olarak işaretle, GameVox da giden verideki mevcut değerini boşaltsın. Değer ne WebSocket frame'inde, ne DOM'da, ne de panelin ekran görüntüsünde olur: yönetici, yer tutucusu “Değişmedi” olan boş, maskeli bir kutu görür.
{
"key": "integrations.apiKey",
"label": "Hava durumu API anahtarı",
"type": "string",
"secret": true,
"help": "Mevcut anahtarı korumak için boş bırak."
} Bundan çıkan sözleşme: boş gönderilen bir secret “değiştirme” demektir, asla “bunu temizle” demek değildir. Bir yöneticinin bir kimlik bilgisini kaldırması gerekiyorsa bunun için ona açık bir boolean ver. string veya text dışındaki herhangi bir türde secret kullanmak yanıtı reddettirir.
Senaryo 9: Salt okunur durum
static bir metin satırı gösterir ve asla gönderilmez. Yöneticinin yapılandırma sırasında ihtiyaç duyduğu ama burada düzenleyemeyeceği durum bilgileri için kullan.
"fields": [
{ "key": "plan", "label": "Plan", "type": "static", "value": "Pro: günde 4.000 sorgu" },
{ "key": "usage", "label": "Bugün kullanılan", "type": "static", "value": "1.284 sorgu" },
{ "key": "lastSync", "label": "Son senkronizasyon", "type": "static", "value": "2026-09-02 14:31 UTC" }
] Senaryo 10: Bir kaydetmeyi işleme
Kaydetme isteği, gönderdiğin alan anahtarlarıyla anahtarlanmış values taşır. Kalıcı olarak kaydet, sonra bir onayla yanıt ver.
// action === "save"
const v = req.values ?? {};
const guild = client.guilds.cache.get(req.server_id);
if (!guild) return reply(nonce, { error: "Bu bot artık o sunucuda değil." });
const patch = {};
// boolean: her zaman gerçek bir boolean
patch.welcomeEnabled = v["welcome.enabled"] === true;
// channel: bir snowflake string'i veya null; BU guild'e karşı yeniden kontrol et
const chan = v["welcome.channel"];
patch.welcomeChannel =
(typeof chan === "string" && guild.channels.cache.has(chan)) ? chan : null;
// number: sayı ya da yönetici kutuyu temizlediyse null
const n = v["automod.threshold"];
patch.threshold = typeof n === "number" ? Math.min(100, Math.max(3, Math.round(n))) : 10;
// secret: boş, değişmedi demektir
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: "Ayarlar kaydedildi." }); message değerin, yöneticinin onay bildirimi olur. Form tam olarak bıraktığı gibi kalır, böylece düzenlemeye devam edebilir.
Senaryo 11: Bir kaydetmeyi reddetme
error döndür, yönetici onay yerine senin metnini görsün. Kabul edemeyeceğin her şey için kullan: kendi doğrulamandan geçmeyen bir değer, bir plan sınırı, artık çalışmayan bir üst servis kimlik bilgisi.
// `required` yalnızca görsel: burada uygula.
const action = v["automod.action"];
if (!["delete", "warn", "timeout", "none"].includes(action)) {
return reply(nonce, guildId, {
error: "Bir filtre eşleştiğinde AutoMod'un ne yapacağını seç.",
});
}
// Yalnızca senin tarafının bilebileceği her şey.
if (patch.apiKey && !(await upstream.verifyKey(patch.apiKey))) {
return reply(nonce, guildId, {
error: "Bu API anahtarı hava durumu sağlayıcısı tarafından reddedildi.",
});
} Aynısı describe için de geçerli: veritabanın çalışmıyorsa yanıt vermemek yerine bir error ile yanıt ver. “Bu uygulama şu anda ayarlarını okuyamadı” mesajı, yönetici için on iki saniyelik bir yükleme simgesinden çok daha iyidir.
Senaryo 12: Eski bir sunucuya hizmet verme
İstek schema_version taşır, yani asla yoklamak zorunda kalmazsın. Sunucunun gerçekten gösterebileceği en zengin formu sun.
const version = typeof req.schema_version === "number" ? req.schema_version : 1;
if (version >= 2) {
await reply(nonce, { version: 2, sections: describeV2(guildId) });
} else {
// v1: düz liste, dört tür: string, boolean, number, select
await reply(nonce, { fields: describeV1(guildId) });
} Tersi de geçerli. Eski bir istemciye ulaşan v2 yanıtı yine gösterilir: GameVox sections ile birlikte düzleştirilmiş bir fields dizisi de gönderir ve yeni türlerden önceki bir istemci bunları metin kutuları olarak çizer. Daha sade görünür ama hiçbir şey gizlenmez.
Senaryo 13: Kendi barındırdığın sunucular
Değişiklik olmadan çalışır. Kendi barındırılan bir sunucuda kanal ve rol katalogları bulut tablolarından değil müşterinin kendi sunucusundan çekilir; bu da uygulamana görünmez: aynı alan türleri, aynı snowflake'ler, aynı yanıt.
Gözlemleyebileceğin tek fark: sunucuya ulaşılamıyorsa seçiciler boş gelir ve “Şu anda kullanılamıyor” olarak gösterilir. Uygulamanın saklı değerleri kaydetmede geri gönderilir, yani hiçbir şey kaybolmaz. Boş bir seçiciyi “yönetici bunu temizledi” olarak yorumlama.
7. GameVox neyi doğrular, neyi doğrulamaz
GameVox bir kaydetmenin biçimini kontrol eder, anlamını ise bilerek kontrol etmez. Gösterdiği formu saklamaz (yapılandırmanın hiçbirini saklamamak zaten bütün mesele), bu yüzden bir kaydetme geldiğinde bizim tarafımızda hangi anahtarın kanal seçicisi, hangisinin serbest metin kutusu olduğunu bilen hiçbir şey yoktur.
| GameVox'un garantileri | Senin yine de kontrol etmen gerekenler |
|---|---|
Anahtarlar anahtar karakter kümesine uyar ve __proto__, constructor ya da prototype değildir | Anahtarın gerçekten senin gönderdiğin bir anahtar olduğunu |
Değerler null, boolean, number, string ya da string dizisidir; asla iç içe bir nesne değildir | Türün, tanımladığın alanla eşleştiğini |
| String'ler sınırlıdır (2.000 karakter) ve diziler en fazla 25 öğe tutar | Kendi daha sıkı sınırlarını |
| Her seçici ID'si, GameVox tarafından bu sunucunun kataloğundan verilmiştir | ID'nin bu guild'de hâlâ çözümlendiğini: kataloglar bir anlık görüntüdür ve bir kanal gösterim ile kaydetme arasında silinebilir |
| Yönetici sunucu sahibidir veya Sunucuyu Yönet iznine sahiptir ve uygulama burada kuruludur | Kendi yetkilendirme kontrollerini (bir plan katmanı, bağlı bir hesap) |
Bu, panelin getirdiği bir açık değil: bir yönetici zaten bir uygulamanın kendi web paneline istediği her şeyi POST edebilir. Sözleşme budur ve seçici türlerinin, bir uygulamanın bir ID'nin ne anlama geldiğine karar vermesine izin vermek yerine sana GameVox'un ürettiği ID'leri vermesinin nedeni de tam olarak budur.
8. Sınırlar
Burada iki davranış var ve aradaki fark önemli: sayılar ve yapı yanıtın tamamını reddettirir, böylece fark edersin; metin tavanları ise sessizce kırpılır, böylece yönetici asla bozuk bir düzen görmez.
| Sınır | Değer | Aşıldığında |
|---|---|---|
| Yanıt başına bölüm | 12 | Reddedilir |
| Yanıt başına alan (bölüm başına değil, toplam) | 60 | Reddedilir |
select / multiselect başına seçenek | 1–25 | Reddedilir |
| Çok değerli alanda seçili öğe | 25 | Reddedilir |
| Alan / bölüm anahtarı | 64 karakter | Reddedilir |
| Seçenek değeri | 64 karakter | Reddedilir |
| Yanıt gövdesi | 256 KB | 413 |
| Etiket, yer tutucu | 100 karakter | Kırpılır |
| Yardım metni | 200 karakter | Kırpılır |
| Bölüm açıklaması | 300 karakter | Kırpılır |
message | 200 karakter | Kırpılır |
error | 300 karakter | Kırpılır |
string ailesinden bir alanın değeri | 1.000 karakter | Kırpılır |
text / static alanının değeri | 2.000 karakter | Kırpılır |
| Bir katalogdaki kanal veya rol | 500 | Kesilir |
Reddetme, yöneticiye aşılan tavanı ve bunu aşan alanı belirterek “Bu uygulama GameVox'un okuyamadığı ayarlar gönderdi: <neden>” şeklinde bildirilir. Tekrarlanan anahtarlar, bilinmeyen bir tür, seçeneği olmayan bir select ve hem sections hem fields göndermek de aynı şekilde reddedilir.
Reddedilmek yerine sessizce düzeltilenler
- Türüne uymayan mevcut bir
valueboşaltılır. Eskimiş bir değer paneli çökertmemeli, yalnızca bir alanı boşaltmalı. - Kullanmayan bir türdeki
optionsatılır. - Sayısal olmayan bir alandaki
min/max/stepatılır. - Tanınmayan
channel_kindsgirdileri atılır. - Bilinmeyen bir anahtarı, kendisini ya da çok değerli bir alanı gösteren bir
show_ifatılır ve alan koşulsuz gösterilir.
9. Zamanlama ve hata durumları
| Davranış | Değer | Ne olur |
|---|---|---|
| Yanıt penceresi | 12 sn | Yöneticiye “Bu uygulama yanıt vermedi. Uygulama içi ayarları desteklemiyor olabilir.” denir. |
| Nonce ömrü | 15 sn | Geç gelen bir yanıt kimseye teslim edilmek yerine atılır. Süresi dolmuş bir nonce ile yanıt vermek 404 döndürür. |
| Bekleme süresi | Eylem başına, uygulama başına, sunucu başına 2 sn | “Uygulamaya biraz zaman tanı, sonra tekrar dene.” describe ve save ayrı pencerelere sahiptir, yani paneli açmak hemen ardından kaydetmeyi engellemez. |
| Uygulama çevrimdışı | - | Baştan reddedilir: “Bu uygulama çevrimdışı, bu yüzden şu anda yapılandırılamaz.” Hiçbir mesaj gönderilmeye çalışılmaz. |
| Yanlış uygulamanın yanıt vermesi | - | 403. Bir nonce, verildiği uygulamaya bağlıdır. |
| Tekrarlanan yanıt | - | İlk yanıt kazanır; ikincisi hiçbir şey yapmaz. |
Her istek uygulamana bir gateway mesajı olarak ulaşır, bu yüzden bekleme süresi, bir yöneticinin GameVox'u kullanarak üçüncü taraf bir botu istek yağmuruna tutmasını önlemek için var. describe handler'ının bütçesini buna göre ayarla: sayfa açıldığında çalışır ve bir veritabanı okuması beklenir, ama on iki saniye kesin sınırdır.
10. Tam örnek (discord.js)
Kendi depolama katmanın hariç, doğrudan kullanılabilir. Referans uygulamanın kullandığı yapının aynısı.
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) {
// Hata durumunda bile yanıt ver: sessizce düşürmek bozuk bir uygulama gibi görünür.
await reply(nonce, guildId, {
error: "Bot bu sunucu için ayarlarını okuyamadı.",
}).catch(() => undefined);
}
}
async function describe(guildId) {
const cfg = await store.get(guildId);
return [
{
key: "general",
label: "Genel",
description: "Botun bu sunucu genelinde nasıl davrandığı.",
fields: [
{ key: "prefix", label: "Komut öneki", type: "string",
value: cfg.prefix, max_length: 4 },
{ key: "ignored", label: "Yok sayılan kanallar", type: "channels",
value: cfg.ignored, channel_kinds: POSTABLE,
help: "Bu kanallarda komutlara yanıt verilmez." },
],
},
{
key: "welcome",
label: "Karşılama mesajları",
description: "Biri katıldığında gönderilir.",
fields: [
{ key: "welcome.enabled", label: "Karşılama mesajı gönder",
type: "boolean", value: cfg.welcome.enabled === true },
{ key: "welcome.channel", label: "Kanal", type: "channel",
value: cfg.welcome.channel ?? null, channel_kinds: POSTABLE,
show_if: { key: "welcome.enabled", equals: true } },
{ key: "welcome.message", label: "Mesaj", type: "text",
value: cfg.welcome.message ?? "", max_length: 1800,
placeholder: "{server} sunucusuna hoş geldin {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 "Ayarlar kaydedildi.";
} 11. Yayına almadan önce kontrol listesi
- Her alan anahtarı sabit. Birinin adını değiştirmek, yöneticinin zaten yapılandırdığı her şeyi sahipsiz bırakır.
- Bir seçiciden gelen her ID, yazılmadan önce guild'e karşı yeniden kontrol ediliyor.
- Boş bir
secrettemizlendi olarak değil, değişmedi olarak ele alınıyor. describeyolu, veritabanı okuması dahil 12 saniyenin çok altında yanıt veriyor.- Her hata yolu yine de yanıt veriyor: sessizlikle değil,
errorile. requiredvemin/maxkaydetme sırasında yeniden uygulanıyor.- Boş bir seçici, yöneticinin alanı temizlediği değil, kataloğun kullanılamadığı anlamına geliyor.
- Handler Discord'da hiçbir şey yapmıyor, yani tek bir build ikisinde de çalışıyor.
Paneli kim açabilir
Sunucu sahibi ya da Sunucuyu Yönet iznine sahip bir üye: uygulama kurma ve kaldırmayı yöneten kısıtlamanın aynısı. Kendi barındırılan bir sunucuda kontrol müşterinin kendi sunucusuna karşı yapılır, böylece oradaki bir yönetici bulutta bir izin satırı olmadığı için reddedilmez.
Uygulaman o sunucuda kurulu değilse GameVox isteği doğrudan reddeder; böylece bir panel isteği yalnızca yöneticinin zaten yetkilendirdiği bir uygulamaya ulaşabilir.