# Formlar

Form, botunun bir komuta ya da butona yanıt olarak kişiye açtığı pencere: kişi en çok beş alanı doldurur, gönderir ve doldurduğu her şey sana yeni bir etkileşim olarak gelir.

> **Uyarı:** Formda şifre, doğrulama kodu, kart bilgisi ya da token isteme. Bu [geliştirici koşullarına](https://noluyo.net/gelistirici/guvenlik.md#formlar) aykırıdır ve botun kapatılır. Kişi her formun altında senin kaldıramayacağın bir satır görür: "Bu form {bot} botuna gider. Şifreni ya da doğrulama kodunu asla yazma." Yalnızca işin için gereken alanı sor.

## Form mu, komut seçeneği mi?

| Durum | Seç |
|---|---|
| Kısa, tek satırlık birkaç değer: bir sayı, bir kişi, listeden bir şey | [Komut seçeneği](https://noluyo.net/gelistirici/komutlar.md) |
| Uzun ya da çok satırlı metin: şikayet, başvuru, açıklama | Form |
| Değeri bir butona basınca sormak | Form (butonun seçeneği yoktur) |
| Kişinin yazdığını önceden doldurup düzelttirmek | Form, `value` ile |
| Yazarken öneri göstermek | [Otomatik tamamlama](https://noluyo.net/gelistirici/otomatik-tamamlama.md); formda öneri yok |

## Akış

| Adım | Ne olur |
|---|---|
| 1 | Kişi komutunu çalıştırır ya da butonuna basar. Sana `INTERACTION_CREATE` gelir, `type: 2` ya da `type: 3`. |
| 2 | 3 saniye içinde `callback` ile `type: 9` ve formu gönderirsin. Yanıt `204`'tür. Bu, etkileşimin tek yanıtıdır. |
| 3 | Form kişinin ekranında açılır; altında sabit uyarı satırı durur. Kişi istediği kadar uzun yazabilir. |
| 4 | Kişi gönderir. Sana yeni bir `INTERACTION_CREATE` gelir, `type: 5`: kendi `id`'si ve 15 dakikalık yeni `token`'ı vardır. |
| 5 | 3 saniye içinde bu yeni etkileşime yanıt verirsin: `4`, `5`, form bir butondan açıldıysa `6` ya da `7` de olur. |

DM'de de aynıdır: komut ya da buton botla açılmış DM'deyse gönderim `context: 1` ile ve `member` yerine `user` ile gelir; kişi gönderim arasında botu engellerse gönderim reddedilir.

Form açıldıktan sonra 1 saat gönderilebilir. Açan etkileşimin 15 dakikalık token'ı bunu kısaltmaz: kişi 40 dakika yazsa da gönderim çalışır.

## Kişi ne görür

> **Not:** Bu bölüm webde gözlenen davranışı anlatır. Masaüstü uygulaması aynı web kodunu kullanır ama ayrıca çalıştırılmadı. Android uygulaması formu kendi tasarımıyla çizer.

Form yalnızca komutu çalıştıran ya da butona basan cihazda açılır; kişinin öbür oturumları aynı olayı alır ama yok sayar. Pencerenin başlığı senin `title`'ındır. Altında botunun avatarı, adı, `BOT` etiketi ve uygulaman doğrulanmışsa "Doğrulanmış" etiketi durur. Alanlar senin sıranla gelir; zorunlu olanların adının yanında kırmızı bir yıldız vardır.

| Alan | Kişi ne görür |
|---|---|
| Metin girişi, `style: 1` | Tek satırlık yazı kutusu, `placeholder` ve `value` ile. Altında kalan karakter sayısı yazar; sınırı geçince sayı kırmızıya döner. `max_length` göndermezsen sınır 4000'dir. |
| Metin girişi, `style: 2` | Çok satırlı, boyu uzatılabilen yazı kutusu; sayaç aynı. |
| Metin menüsü | Tek seçimliyse uygulamanın açılır listesi, çok seçimliyse işaretlenen bir liste penceresi. `default` seçenekler seçili gelir. |
| Kullanıcı, rol, kanal, karma menü | Bir düğme; basınca aramalı bir liste penceresi açılır, kişi işaretleyip "Onayla"ya basar. Kanal listesinde yalnızca kişinin gördüğü ve `channel_types`'a uyan kanallar çıkar. `required: false` menü boş bırakılabilir. |

Pencerenin altında, alanların dışında, kişinin her zaman gördüğü satır durur: "Bu form {bot} botuna gider. Şifreni ya da doğrulama kodunu asla yazma." Alanlar uzunsa yalnızca alanlar kayar; bu satır ve düğmeler yerinde kalır.

Kişi "Gönder"e basınca uygulama önce zorunlu alanları ve uzunlukları sunucunun kurallarıyla denetler: yalnızca boşluk yazılmış zorunlu alan boş sayılır. Hata alanın altında kırmızı yazar ve form gitmez. Her şey tamsa "Gönder" dönen bir simgeye döner ve botun yanıtlayana kadar bekler; yanıt gelince form kapanır.

Gönderim başarısız olursa form açık kalır ve kişinin yazdığı her şey yerinde durur:

| Ne oldu | Kişi ne görür |
|---|---|
| Botun 3 saniyede yanıt vermedi | Formun altında "Bot yanıt vermedi." Kişi yeniden gönderebilir. |
| Sunucu bir alanı reddetti (`validation_failed`) | Hata o alanın altında. |
| Form süresi doldu ya da üç kez gönderildi (`modal_expired`) | "Süre doldu, metni kopyalayıp komutu yeniden çalıştır." "Gönder" kapanır, yanında bir "Kopyala" düğmesi çıkar; yazılan metin alan adlarıyla birlikte panoya kopyalanır. |
| Başka bir hata | Hatanın metni formun altında. |

Kişi bir şey yazmışsa kapatma düğmesi, `Esc`, dışarı tıklama ya da "Vazgeç" önce "Yazdıkların silinsin mi?" diye sorar. Yalnızca senin `value` ile doldurduğun metin duruyorsa form sormadan kapanır.

## En küçük örnek

Komuta form ile yanıt ver:

```bash
curl -X POST "https://noluyo.net/api/v1/interactions/$INTERACTION_ID/$INTERACTION_TOKEN/callback" \
  -H "Authorization: Bot $BOT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"type":9,"data":{"custom_id":"sikayet","title":"Şikayet","components":[{"type":1,"components":[{"type":4,"custom_id":"konu","style":1,"label":"Konu","max_length":100}]},{"type":1,"components":[{"type":4,"custom_id":"detay","style":2,"label":"Ne oldu?","required":false}]}]}}'
```

Aynı form JSON olarak:

```json
{
  "type": 9,
  "data": {
    "custom_id": "sikayet",
    "title": "Şikayet",
    "components": [
      { "type": 1, "components": [
        { "type": 4, "custom_id": "konu", "style": 1, "label": "Konu", "max_length": 100 }
      ] },
      { "type": 1, "components": [
        { "type": 4, "custom_id": "detay", "style": 2, "label": "Ne oldu?", "required": false }
      ] }
    ]
  }
}
```

## Formun alanları

| Alan | Kural |
|---|---|
| `custom_id` | Zorunlu, en çok 100 karakter. Gönderimde `data.custom_id` olarak geri gelir. |
| `title` | Zorunlu, en çok 45 karakter |
| `components` | 1-5 eylem satırı (`type: 1`); her satırda tek bir metin girişi ya da tek bir menü |

Formun JSON'u en çok 16 KiB olabilir. Alanların `custom_id`'leri form içinde tekil olmalı. Otomatik moderasyon başlığı, etiketleri, yer tutucuları ve ön doldurduğun değerleri okur; kuralı tutan form `automod_blocked` ile reddedilir.

## Metin girişi

| Alan | Kural |
|---|---|
| `type` | `4` |
| `custom_id` | Zorunlu, en çok 100 karakter |
| `style` | `1` tek satır, `2` paragraf |
| `label` | Zorunlu, en çok 45 karakter |
| `placeholder` | Boşken görünen yazı, en çok 100 karakter |
| `min_length` | 0-4000, varsayılan 0 |
| `max_length` | 1-4000, varsayılan 4000 |
| `required` | Varsayılan `true` |
| `value` | Form açılırken alanda dolu görünen metin, en çok `max_length`; kişi değiştirebilir |

Zorunlu alana yalnız boşluk yazılırsa gönderim reddedilir ve kişi alanı doldurması gerektiğini görür. Bunun dışında metin yazıldığı gibi gelir: baştaki ve sondaki boşluk silinmez, satır sonları korunur. İsteğe bağlı alan boş bırakıldıysa `value` boş dizedir.

## Formda menü

Bir satır metin girişi yerine bir menü de taşıyabilir: metin (`3`), kullanıcı (`5`), rol (`6`), kullanıcı ya da rol (`7`), kanal (`8`). [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md#menuler) sayfasındaki kurallar geçerlidir; formda iki fark vardır:

| Alan | Formda |
|---|---|
| `label` | Zorunlu, en çok 45 karakter |
| `required` | Varsayılan `true`; `false` ise kişi menüyü boş bırakabilir, o zaman `values` boş liste gelir |
| `disabled` | Kullanılmaz |

Sunucu seçilenleri kişinin erişebildiğine göre denetler: kullanıcı sunucunun üyesi, rol bu sunucunun, kanal kişinin görebildiği olmalı. Seçilen kullanıcılar, üyeler, roller ve kanallar `data.resolved` içinde gelir.

## Ön doldurma

`value` ile kişinin daha önce yazdığını ya da senin bildiğin bir değeri alana koyarsın; kişi onaylar ya da düzeltir. Örneğin bir butondan "düzenle" formu açarken eski metni geri ver:

```json
{ "type": 4, "custom_id": "aciklama", "style": 2, "label": "Açıklama", "value": "Eski açıklama burada." }
```

Kullanıcı, rol ve kanal menüsünde `default_values` aynı işi görür.

## Gönderim sana nasıl gelir

```json
{
  "id": "111804206284800777",
  "application_id": "14631410073600008",
  "type": 5,
  "token": "eyJpZCI6IjExMTgwNDIwNjI4NDgwMDc3NyJ9.bWFj",
  "version": 1,
  "guild_id": "21531879014400001",
  "channel_id": "21531887403008007",
  "member": {
    "user": { "id": "14631410073600003", "username": "ayse", "display_name": "Ayşe", "avatar": null, "cosmetics": null },
    "nick": null, "roles": [], "joined_at": "2026-10-01T12:00:00Z", "permissions": "2147483648"
  },
  "app_permissions": "2048",
  "locale": "tr",
  "data": {
    "custom_id": "sikayet",
    "components": [
      { "type": 1, "components": [{ "type": 4, "custom_id": "konu", "value": "Spam yapan biri" }] },
      { "type": 1, "components": [{ "type": 4, "custom_id": "detay", "value": "Genel kanalda\nreklam atıyor." }] },
      { "type": 1, "components": [{ "type": 5, "custom_id": "kim", "values": ["14631410073600005"] }] }
    ],
    "resolved": {
      "users": { "14631410073600005": { "id": "14631410073600005", "username": "spamci", "display_name": null, "avatar": null, "cosmetics": null } }
    }
  }
}
```

| Alan | Anlamı |
|---|---|
| `type` | `5`: gönderilen form |
| `token` | Bu gönderimin kendi token'ı, 15 dakika geçerli; yanıt ve takip mesajları bununla |
| `data.custom_id` | Açtığın formun `custom_id`'si |
| `data.components` | Formun her alanı kendi sırasıyla, doldurulmuş ya da boş; metin girişi `value`, menü `values` taşır |
| `data.resolved` | Menülerde seçilen kullanıcı, üye, rol ve kanal; menü yoksa yer almaz |
| `member` ya da `user`, `locale` | Gönderen kişi ve dili |

Formu bir komut açtıysa yanıtın mesajı "X, /komut kullandı" diye gösterilir.

## Yanıt seçenekleri

| Etkileşim | `type: 4` | `type: 5` | `type: 6` | `type: 7` | `type: 9` |
|---|---|---|---|---|---|
| Komut çalıştırma (`type: 2`) | Evet | Evet | Hayır | Hayır | Evet |
| Butona basma, menüden seçme (`type: 3`) | Evet | Evet | Evet | Evet | Evet |
| Otomatik tamamlama (`type: 4`) | Hayır | Hayır | Hayır | Hayır | Hayır |
| Komuttan açılmış formun gönderimi (`type: 5`) | Evet | Evet | Hayır | Hayır | Hayır |
| Butondan açılmış formun gönderimi (`type: 5`) | Evet | Evet | Evet | Evet | Hayır |

`type: 7` butondan açılmış formda butonun durduğu mesajı günceller; o mesaj ephemeral ise ephemeral mesaj bütünüyle değişir. `flags: 64` ile `4` ya da `5` yanıtı yalnızca gönderen görür. Ayrıntılar [Etkileşimler](https://noluyo.net/gelistirici/etkilesimler.md) sayfasında.

## Kurallar

- **Formu açan bir form açamaz.** Gönderilen forma `type: 9` ile yanıt veremezsin. Başka soruların varsa ilk formu en çok beş alanla tasarla ya da yanıtına bir buton koy; buton yeni bir form açabilir.
- **Formu açmak ilk yanıttır.** Önce `type: 5` ya da `type: 6` verip sonra form açamazsın: 3 saniye içinde doğrudan `type: 9` gönder. Formu hazırlamak için dışarıya gitme; tanımı kodunda hazır tut.
- **Form yazmaz.** `type: 9` kanala mesaj yazmaz ve tüm botların [ortak yazma tavanına](https://noluyo.net/gelistirici/rate-limit.md) sayılmaz.
- **Bir form üç kez gönderilebilir.** Botun yanıt vermezse kişi yazdığını kaybetmeden yeniden gönderebilir. Her gönderim sana ayrı bir etkileşim olarak gelir; aynı içerik ikinci kez gelirse işini iki kez yapma. Üçüncüden sonra kişi `modal_expired` alır ve formu yeniden açması gerekir.
- **Gönderimde her şey yeniden denetlenir.** Kişi kanala hâlâ erişebilmeli, zaman aşımında olmamalı, botun sunucuda olmalı ve form bir komuttan açıldıysa komutun izin kuralları kişiye hâlâ izin vermeli. Form sen açtığından beri değişmemiştir: kişi alan ekleyemez, `custom_id` değiştiremez.
- **Yazılan saklanmaz.** N'oluyo formun içeriğini saklamaz; yalnızca kişinin bir form gönderdiğini erişim kaydına yazar. İçerik sana gelir; yalnızca işin için kullan.

## Tam örnek: şikayet formu botu

Node.js 22 veya üstü yeter. [Gateway](https://noluyo.net/gelistirici/gateway.md#tam-bir-istemci) sayfasındaki `gateway.mjs` ile [Örnekler](https://noluyo.net/gelistirici/ornekler.md) sayfasındaki `api.mjs` aynı klasörde olmalı. Bot `/sikayet` komutunu kaydeder, form açar, gönderilen şikayeti moderatör kanalına yazar ve kişiye yalnızca onun göreceği bir teşekkür gönderir:

```js title=sikayet-botu.mjs
import { connect } from "./gateway.mjs";
import { call } from "./api.mjs";

// Moderatörlerin gördüğü kanalın kimliği.
const modChannel = process.env.MOD_CHANNEL_ID;

const commands = [{
  name: "sikayet",
  name_localizations: { en: "report" },
  description: "Moderatörlere şikayet gönder",
  description_localizations: { en: "Send a report to the moderators" },
}];

const form = (en) => ({
  custom_id: "sikayet",
  title: en ? "Report" : "Şikayet",
  components: [
    { type: 1, components: [{ type: 5, custom_id: "kim", label: en ? "Who?" : "Kim?" }] },
    { type: 1, components: [{ type: 4, custom_id: "konu", style: 1, label: en ? "Subject" : "Konu", max_length: 100 }] },
    { type: 1, components: [{ type: 4, custom_id: "detay", style: 2, label: en ? "What happened?" : "Ne oldu?",
      required: false, max_length: 1500 }] },
  ],
});

// Gönderilen formun alanları: custom_id -> value ya da values.
function fields(data) {
  const out = {};
  for (const row of data.components) for (const f of row.components) out[f.custom_id] = f.value ?? f.values;
  return out;
}

async function onInteraction(d) {
  const path = `/interactions/${d.id}/${d.token}/callback`;
  const en = d.locale === "en";
  if (d.type === 2 && d.data.name === "sikayet") {
    // Formu açmak ilk yanıttır: önce 5 verilmez.
    await call("POST", path, { type: 9, data: form(en) });
    return;
  }
  if (d.type !== 5 || d.data.custom_id !== "sikayet") return;
  const f = fields(d.data);
  const who = d.data.resolved?.users?.[f.kim[0]];
  const from = d.member?.user ?? d.user;
  await call("POST", path, { type: 5, data: { flags: 64 } });
  try {
    await call("POST", `/channels/${modChannel}/messages`, {
      embeds: [{
        title: f.konu,
        description: f.detay || "-",
        fields: [
          { name: "Şikayet edilen", value: `<@${who.id}>`, inline: true },
          { name: "Gönderen", value: `<@${from.id}>`, inline: true },
        ],
      }],
      allowed_mentions: { parse: [] },
    });
    await call("PATCH", `/interactions/${d.id}/${d.token}/messages/@original`, {
      content: en ? "Thanks, the moderators got your report." : "Teşekkürler, şikayetin moderatörlere ulaştı.",
    });
  } catch (err) {
    console.error(err.message);
    await call("PATCH", `/interactions/${d.id}/${d.token}/messages/@original`, {
      content: en ? "Your report could not be delivered. Try again later." : "Şikayetin iletilemedi. Biraz sonra yeniden dene.",
    });
  }
}

let registered = false;

await connect({
  intents: 0,
  onEvent(t, d) {
    if (t === "READY" && !registered) {
      registered = true;
      call("PUT", `/applications/${d.application.id}/commands`, commands)
        .then(() => console.log("/sikayet kaydedildi"))
        .catch(console.error);
    }
    if (t === "INTERACTION_CREATE") onInteraction(d).catch((err) => console.error(err.message));
  },
});
```

Çalıştırmak için botunun moderatör kanalında `SendMessages` ve `EmbedLinks` izni olsun:

```bash
BOT_TOKEN='kopyaladigin_token' MOD_CHANNEL_ID='kanal_kimligi' node sikayet-botu.mjs
```

Örnek şikayetin içeriğini konsola yazmaz. Moderatör kanalına yazma 3 saniyeyi aşabilir; bu yüzden önce `type: 5` ile yanıt verip sonucu `@original` ile doldurur.

## Limitler

| Anahtar | Değer | Saniye | Anlamı |
|---|---|---|---|
| `title` | 45 | | Başlık, karakter |
| `rows` | 5 | | Formdaki alan sayısı |
| `label` | 45 | | Alan etiketi, karakter |
| `placeholder` | 100 | | Metin girişinin yer tutucusu, karakter |
| `value` | 4000 | | Metin girişine yazılabilen, karakter |
| `bytes` | 16384 | | Formun JSON'u |
| `sends` | 3 | 3600 | Bir formun gönderilebileceği sayı ve formun ömrü |
| `user` | 5 | 5 | Kişi başına kanalda basma, komut ve form gönderimi birlikte |

Kişinin gönderimleri basma ve komutlarla aynı sınırları paylaşır; aşılırsa sana bir şey gelmez, kişi `429` alır ve form açık kalır.

## Hatalar

Formu açarken sana dönenler:

| Kod | Ne oldu | Ne yap |
|---|---|---|
| `interaction_response_invalid` | `type: 9`'u otomatik tamamlamaya ya da gönderilen forma verdin, `data` yok, ya da yanında `content`, `embeds`, `flags` gibi mesaj alanı var | Formu yalnızca komuta ya da basmaya, yalnızca `custom_id`, `title` ve `components` ile gönder. |
| `validation_failed` | Form geçersiz: örneğin başlık yok, beşten çok satır, bir satırda iki alan, etiket 45 karakteri aşıyor | `errors` alanına bak. Hakkın gitmedi, düzeltip yeniden gönder. |
| `automod_blocked` | Başlık, etiket ya da ön doldurduğun değer sunucunun otomatik moderasyon kuralına takıldı | Metni değiştir. |
| `interaction_already_acknowledged` | Bu etkileşime zaten yanıt verdin, örneğin önce `type: 5` | Formu ilk yanıt olarak gönder. |
| `interaction_timeout` | 3 saniye geçti | Formu kodunda hazır tut, açmadan önce dışarıya gitme. |

Kişinin gönderirken alabileceği hatalar sana gelmez:

| Kod | Ne oldu |
|---|---|
| `modal_expired` | Form 1 saatten eski, üç kez gönderildi, ya da başka bir kişinin veya kanalın |
| `validation_failed` | Zorunlu alan boş ya da yalnız boşluk, uzunluk dışında, menüde kişinin erişemediği bir değer |
| `missing_access`, `missing_permissions` | Kişi kanalı artık göremiyor ya da zaman aşımında |
| `application_unavailable` | Botun sunucudan çıkarıldı ya da kapatıldı |
| `command_forbidden` | Form bir komuttan açıldı ve komutun izin kuralları artık kişiye izin vermiyor |
| `rate_limited`, `interaction_busy` | Kişinin ya da tüm botların sınırı doldu |

## Bu sayfayı yapay zekaya verirsen

> **İpucu:** Bu sayfayı bir yapay zeka asistanına verip kod yazdırıyorsan şu kuralları da ekle:
>
> - Token koda yazılmaz, `process.env.BOT_TOKEN`'dan okunur.
> - Formda şifre, doğrulama kodu, kart bilgisi ya da token istenmez; yalnızca işin gerektirdiği alanlar sorulur.
> - Form, komuta (`type: 2`) ya da basmaya (`type: 3`) 3 saniye içinde ilk yanıt olarak `{"type":9,"data":{"custom_id","title","components"}}` ile açılır; önce `type: 5` verilmez.
> - En çok 5 satır, her satırda tek metin girişi (`type: 4`) ya da tek menü; başlık ve etiket en çok 45 karakter.
> - Gönderim `INTERACTION_CREATE` `type: 5` olarak kendi `id` ve `token`'ıyla gelir ve 3 saniye içinde yanıtlanır; yanıt `type: 9` olamaz.
> - Alanlar `data.components[].components[]` içindedir: metin girişi `value`, menü `values` taşır; seçilenler `data.resolved`'dadır.
> - Aynı form üç kez gelebilir; iş iki kez yapılmaz. Formun içeriği konsola yazılmaz.

## Sırada ne var

- [Komutlar](https://noluyo.net/gelistirici/komutlar.md): form açabilen eğik çizgi, kullanıcı ve mesaj komutları
- [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md): form açabilen butonlar ve seçim menüleri
- [Etkileşimler](https://noluyo.net/gelistirici/etkilesimler.md): form gönderimine verilen yanıt türleri ve takip mesajları
