Geliştirici Portalı
Deneysel Bot ve uygulama platformu aktif olarak geliştiriliyor. Kendi barındırdığın sunucu desteği 13.08.2026'da yayınlandı ve buluta göre daha az özellik sunuyor. Nelerin desteklendiğine bak.
← Dokümanlar

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.

GameVox istemcisinin Sunucu Ayarları penceresi; kurulu bir uygulamanın Ayarlar sekmesinde bir dil açılır listesi, bir anahtar ve bir kanal onay listesi görünüyor.
Tamamen tek bir botun yanıtından oluşturulmuş canlı bir panel: açıklamalı bir bölümde gruplanmış bir 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.

DiscordGameVox
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
  }
}
AlanTürNotlar
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üphaneHookEtkinleş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.

AnahtarTürNotlar
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 valueKaydetmede 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

ÖznitelikUygulandığı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.

Sunucunun kendi kanallarından oluşan, her biri türünü gösteren bir simgeyle başlayan bir onay listesi olarak gösterilen bir channels alanı.
Uygulamanın bu alan için gönderdiği her şey { "key": "ignored", "type": "channels", "channel_kinds": ["text", "news", "forum", "fileshare"] } idi. Liste, tür simgeleri ve ID'ler GameVox'tan geliyor.
DurumYö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 garantileriSenin 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ırDeğerAşı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 value boşaltılır. Eskimiş bir değer paneli çökertmemeli, yalnızca bir alanı boşaltmalı.
  • Kullanmayan bir türdeki options atılır.
  • Sayısal olmayan bir alandaki min / max / step atılır.
  • Tanınmayan channel_kinds girdileri atılır.
  • Bilinmeyen bir anahtarı, kendisini ya da çok değerli bir alanı gösteren bir show_if atılır ve alan koşulsuz gösterilir.

9. Zamanlama ve hata durumları

DavranışDeğerNe 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 secret temizlendi olarak değil, değişmedi olarak ele alınıyor.
  • describe yolu, veritabanı okuması dahil 12 saniyenin çok altında yanıt veriyor.
  • Her hata yolu yine de yanıt veriyor: sessizlikle değil, error ile.
  • required ve min/max kaydetme 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.

← Discord'dan geçiş  ·  Dokümanlara dön