# Örnekler

Bu sayfadaki altı bot, bugünkü API ile çalışır ve Node.js 22 veya üstü dışında bir şey istemez: bu sürümlerde `WebSocket` hazırdır, ek paket gerekmez. İlk üçü mesajlarla çalışır; son üçü buton, menü ve embed kullanır. Hepsi aynı küçük yardımcı dosyaları kullanır. Ses kanalında müzik veya ses dosyası çalan bot örneği ise LiveKit paketi kullandığı için [Ses](https://noluyo.net/gelistirici/ses.md#odaya-baglanmak-ve-dosya-calmak) sayfasındadır. curl örnekleri için ayrıca [jq](https://jqlang.org/) gerekir.

Başlamadan önce botunu oluşturup sunucuna ekle ([Hızlı başlangıç](https://noluyo.net/gelistirici/baslangic.md) bunu adım adım anlatır) ve token ile kanal ID'sini terminalde tanımla. Kanal ID'si zamanlı duyuru ve butonlu üç bot için gerekir; rol seçici sunucu ID'sini de ister:

```bash
export BOT_TOKEN='kopyaladigin_token'
export CHANNEL_ID='kanal id degeri'
```

> **Not:** Mesajları okuyan iki bot için portalda uygulama sayfasının **Ayrıcalıklı intent'ler** bölümünden **Mesaj içeriği**'ni aç. Açmazsan bağlantı `4014` ile kapanır. Neden gerektiği [Intent'ler](https://noluyo.net/gelistirici/intentler.md) sayfasında.

## Ortak yardımcılar

Olayları dinlemek için [Gateway](https://noluyo.net/gelistirici/gateway.md#tam-bir-istemci) sayfasının sonundaki `gateway.mjs` dosyasını bu klasöre kaydet.

İstek göndermeyi tek yerde toplayan bu dosyayı `api.mjs` adıyla kaydet. `429` gelince söylenen süre kadar bekleyip yeniden dener, başka bir hatada ise durum ve kodu içeren bir hata fırlatır.

```js title=api.mjs
const API = "https://noluyo.net/api/v1";
const { BOT_TOKEN } = process.env;

export const sleep = (ms) => new Promise((done) => setTimeout(done, ms));

export async function call(method, path, body) {
  for (;;) {
    const res = await fetch(API + path, {
      method,
      headers: {
        Authorization: `Bot ${BOT_TOKEN}`,
        ...(body ? { "Content-Type": "application/json" } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (res.status === 204) return null;
    const data = await res.json();
    if (res.status === 429) {
      await sleep(data.retry_after * 1000);
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.message}`);
    return data;
  }
}
```

## !ping yazılınca yanıt ver

Botun görebildiği her kanalı dinler ve `!ping` yazan mesajlara `pong!` diye yanıt verir.

**Gereken izinler:** Kanalları görüntüle, Mesaj gönder, Mesaj geçmişini oku. Bunların toplamı `68608` eder: ekleme bağlantısının sonu `izin=68608` olur.

**Gereken intent'ler:** `GUILD_MESSAGES` ve `MESSAGE_CONTENT`.

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

let me;

await connect({
  intents: (1 << 9) | (1 << 15),
  async onEvent(t, d) {
    if (t === "READY") me = d.user.id;
    if (t !== "MESSAGE_CREATE" || d.author.id === me || d.content !== "!ping") return;
    // Tek bir hata botu durdurmasın.
    try {
      await call("POST", `/channels/${d.channel_id}/messages`, {
        content: "pong!",
        message_reference: { message_id: d.id },
      });
    } catch (err) {
      console.error(err.message);
    }
  },
});
```

Çalıştır ve kanala `!ping` yaz:

```bash
node ping.mjs
```

`message_reference`, gönderilen mesajı yanıt yapar; bunun için **Mesaj geçmişini oku** gerekir. Portalda **Mesaj içeriği** kapalıyken bu kod `4014` ile çıkar. `MESSAGE_CONTENT`'i `intents`'ten de çıkarırsan bağlanır, ama `content` boş gelir ve `!ping` hiç eşleşmez; yalnızca botu etiketleyen mesajların metni görünür.

## Zamanlı duyuru gönder

Belirli aralıklarla bir kanala aynı mesajı gönderir. Bu bot gateway kullanmaz, REST yeterlidir: hiçbir olaya yanıt vermez, yalnızca saati gelince yazar. Açık bağlantıya ihtiyacı olmadığı için `cron` gibi bir zamanlayıcıyla da çalışır. Kanalı okumadığı için geçmiş iznine de ihtiyacı yoktur.

**Gereken izinler:** Kanalları görüntüle, Mesaj gönder. Toplamları `3072` eder: `izin=3072`.

```js tab=JavaScript
// duyuru.mjs
import { call } from "./api.mjs";

const EVERY_HOUR = 60 * 60 * 1000;
const path = `/channels/${process.env.CHANNEL_ID}/messages`;

async function announce() {
  // Tek bir hata zamanlayıcıyı durdurmasın.
  try {
    const sent = await call("POST", path, { content: "Etkinlik bu akşam 21.00'de!" });
    console.log("gönderildi", sent.id);
  } catch (err) {
    console.error(err.message);
  }
}

await announce();
setInterval(announce, EVERY_HOUR);
```

```bash tab=curl
# Her gün 09.00'da göndermek için crontab'a ekle. BOT_TOKEN ve CHANNEL_ID
# cron'un ortamında tanımlı olmalı; onları crontab'ın başına da yazabilirsin.
0 9 * * * curl -s -o /dev/null -X POST "https://noluyo.net/api/v1/channels/$CHANNEL_ID/messages" -H "Authorization: Bot $BOT_TOKEN" -H "Content-Type: application/json" -d '{"content":"Günaydın, bugünün duyurusu burada."}'
```

Kanalda yavaş mod açıksa ve aralık ondan kısaysa `429 slowmode` alırsın; `api.mjs` söylenen süre kadar bekleyip yeniden dener.

`@everyone` ile herkese bildirim göndermek istersen bota **@herkes ve rolleri etiketle** iznini de ver: `3072 + 131072 = 134144`. Bu izin olmadan `@everyone` düz yazı olarak kalır ve kimseye bildirim gitmez.

## Belirli bir sözcüğü içeren mesajları sil

Yeni mesajları dinler ve listedeki sözcüklerden birini içerenleri siler.

**Gereken izinler:** Kanalları görüntüle, Mesaj geçmişini oku, Mesajları yönet. Toplamları `74752` eder: `izin=74752`.

**Gereken intent'ler:** `GUILD_MESSAGES` ve `MESSAGE_CONTENT`.

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

const WORDS = ["reklam", "bedava üyelik"];
const lower = (text) => text.toLocaleLowerCase("tr-TR");

let me;

await connect({
  intents: (1 << 9) | (1 << 15),
  async onEvent(t, d) {
    if (t === "READY") me = d.user.id;
    if (t !== "MESSAGE_CREATE" || d.author.id === me) return;
    const text = lower(d.content);
    if (!WORDS.some((word) => text.includes(word))) return;
    try {
      await call("DELETE", `/channels/${d.channel_id}/messages/${d.id}`);
      console.log("silindi", d.id);
    } catch (err) {
      // Başka biri önce silmiş olabilir: 404 unknown_message.
      console.error(d.id, err.message);
    }
  },
});
```

Olaylar yalnızca bot çalışırken yazılanları getirir. Daha önceki mesajlar için kanalı REST ile bir kez tarayabilirsin. Bu okuma da aynı içerik kuralına uyar: **Mesaj içeriği** kapalıysa metinler boş gelir ve hiçbir şey eşleşmez.

```bash
# temizle.sh; son 100 mesajı bir kez tarar ve eşleşenleri siler.
API="https://noluyo.net/api/v1"
AUTH="Authorization: Bot $BOT_TOKEN"
MESSAGES="$API/channels/$CHANNEL_ID/messages"

curl -s "$MESSAGES?limit=100" -H "$AUTH" \
  | jq -r '.[] | select(.content | ascii_downcase | contains("reklam")) | .id' \
  | while read -r ID; do
      curl -s -o /dev/null -w "$ID %{http_code}\n" -X DELETE "$MESSAGES/$ID" -H "$AUTH"
      sleep 1
    done
```

Bilmen gerekenler:

- Mesaj silmek için rol sırası önemli değildir; `ManageMessages` yeterlidir. Kanal ayarları bu izni kaldırabilir, o zaman `403 missing_permissions` alırsın.
- Silme rotaları `write` sınıfındadır: kanal başına 10 saniyede 10 istek. `api.mjs` sınır aşılınca bekler.
- Başkasının mesajını silmek, sunucunun denetim kaydına botun adıyla yazılır.

Çok mesajı tek istekte silmek için [toplu silme](https://noluyo.net/gelistirici/api-messages.md#bulkDeleteMessages) rotası var. 2 ile 100 arası mesaj alır, hiçbiri 14 günden eski olamaz ve saniyede bir istek kabul eder:

```bash
curl -i -X POST "https://noluyo.net/api/v1/channels/$CHANNEL_ID/messages/bulk-delete" \
  -H "Authorization: Bot $BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages":["111600000000000000","111600000000000100"]}'
```

## Butonla anket

Kanala üç butonlu bir anket atar. Her basmada oyu sayar ve aynı mesajı `type: 7` ile yeni sayılarla günceller. Kişi başka butona basarsa oyu oraya geçer, iki kez oy veremez. Basmanın kendisi ve yanıt türleri [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md) ile [Etkileşimler](https://noluyo.net/gelistirici/etkilesimler.md) sayfalarında.

**Gereken izinler:** Kanalları görüntüle, Mesaj gönder. Toplamları `3072` eder: `izin=3072`. Embed için ayrı izin gerekmez.

**Gereken intent'ler:** Yok. `INTERACTION_CREATE` her bağlantıya gelir.

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

const { CHANNEL_ID } = process.env;
if (!CHANNEL_ID) throw new Error("CHANNEL_ID tanımlı olmalı");

const QUESTION = "Bu akşam ne izleyelim?";
const OPTIONS = ["Film", "Dizi", "Maç"];
// Kim neye oy verdi: kişinin kimliği ve seçeneğin sırası. Bot yeniden başlarsa sıfırlanır.
const votes = new Map();

// Mesajın gövdesi: ilk gönderimde ve her güncellemede aynı biçim.
function view() {
  const count = (i) => [...votes.values()].filter((v) => v === i).length;
  return {
    content: QUESTION,
    embeds: [{
      color: parseInt("3366FF", 16),
      fields: OPTIONS.map((name, i) => ({ name, value: `${count(i)} oy`, inline: true })),
      footer: { text: `${votes.size} kişi oy verdi. Fikrin değişirse başka butona bas.` },
    }],
    components: [{ type: 1, components: OPTIONS.map((name, i) => (
      { type: 2, style: 1, label: name, custom_id: `anket:${i}` }
    )) }],
  };
}

async function onPress(d) {
  const [kind, choice] = d.data.custom_id.split(":");
  if (kind !== "anket") return;
  // Sunucuda member.user, DM'de user gelir.
  const who = (d.member?.user ?? d.user).id;
  votes.set(who, Number(choice));
  // type 7: butonun durduğu mesajı yeni sayılarla günceller.
  await call("POST", `/interactions/${d.id}/${d.token}/callback`, { type: 7, data: view() });
}

let posted = false;

await connect({
  intents: 0,
  onEvent(t, d) {
    if (t === "READY" && !posted) {
      posted = true;
      call("POST", `/channels/${CHANNEL_ID}/messages`, view()).catch(console.error);
    }
    if (t === "INTERACTION_CREATE") onPress(d).catch(console.error);
  },
});
```

Çalıştır, kanalda butonlara bas:

```bash
node anket.mjs
```

Bilmen gerekenler:

- Oylar programın belleğinde durur; bot yeniden başlarsa sıfırlanır ve yeni bir anket atar. Kalıcı olması gerekiyorsa oyları kendi veritabanında tut.
- Embed değiştiği için mesaj "(düzenlendi)" olarak işaretlenir.
- Kanalı gören herkes oy verebilir, `SendMessages` izni olmayan biri de.

## Rol seçici

Kanala bir buton atar. Basan kişi, yalnızca kendisinin gördüğü bir menüde rollerini işaretler. Bot işaretlenen rolleri verir, işareti kaldırılanları geri alır. Menü ephemeral olduğu için kanal kalabalıklaşmaz: herkes kendi menüsünü görür.

**Gereken izinler:** Kanalları görüntüle, Mesaj gönder, Rolleri yönet. Toplamları `268438528` eder: `izin=268438528`. Dağıtılacak roller botun rolünün altında olmalı; değilse rol istekleri `403 role_hierarchy` döner.

**Gereken intent'ler:** Yok.

```bash
export GUILD_ID='sunucu id degeri'
export ROLE_IDS='rol id,diger rol id'
```

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

const { CHANNEL_ID, GUILD_ID, ROLE_IDS } = process.env;
if (!CHANNEL_ID || !GUILD_ID || !ROLE_IDS) throw new Error("CHANNEL_ID, GUILD_ID ve ROLE_IDS tanımlı olmalı");

// Menüde rollerin adları görünür; açılışta bir kez okunur.
const wanted = ROLE_IDS.split(",");
const roles = (await call("GET", `/guilds/${GUILD_ID}/roles`)).filter((r) => wanted.includes(r.id));
if (roles.length === 0) throw new Error("ROLE_IDS içindeki roller sunucuda yok");

// Yalnızca basan kişinin gördüğü menü. Kişinin şimdiki rolleri işaretli gelir.
const menu = (content, has) => ({
  content,
  flags: 64,
  components: [{ type: 1, components: [{
    type: 3,
    custom_id: "roller",
    placeholder: "Rol seç",
    min_values: 0,
    max_values: roles.length,
    options: roles.map((r) => ({ label: r.name, value: r.id, default: has.includes(r.id) })),
  }] }],
});

async function onPress(d) {
  if (!d.member) return;
  const answer = (body) => call("POST", `/interactions/${d.id}/${d.token}/callback`, body);
  const has = d.member.roles;

  if (d.data.custom_id === "roller-ac") {
    await answer({ type: 4, data: menu("Almak istediğin rolleri işaretle, bırakacaklarının işaretini kaldır.", has) });
    return;
  }
  if (d.data.custom_id !== "roller") return;

  const picked = d.data.values;
  const add = roles.filter((r) => picked.includes(r.id) && !has.includes(r.id));
  const drop = roles.filter((r) => !picked.includes(r.id) && has.includes(r.id));
  const names = roles.filter((r) => picked.includes(r.id)).map((r) => r.name).join(", ") || "hiçbiri";

  // Önce yanıt ver: rol istekleri 3 saniyeyi aşabilir. Ephemeral mesajın tamamı gönderilir.
  await answer({ type: 7, data: menu(`Rollerin: ${names}`, picked) });

  const path = (r) => `/guilds/${d.guild_id}/members/${d.member.user.id}/roles/${r.id}`;
  try {
    for (const r of add) await call("PUT", path(r));
    for (const r of drop) await call("DELETE", path(r));
  } catch (err) {
    console.error(err.message);
    // Takip mesajı: ilk yanıttan sonra 15 dakika gönderilebilir. Kişiye hatanın ayrıntısı değil, sade bir metin gider.
    await call("POST", `/interactions/${d.id}/${d.token}/messages`, { content: "Rollerin şu an değiştirilemedi. Biraz sonra yeniden dene.", flags: 64 });
  }
}

let posted = false;

await connect({
  intents: 0,
  onEvent(t, d) {
    if (t === "READY" && !posted) {
      posted = true;
      call("POST", `/channels/${CHANNEL_ID}/messages`, {
        content: "Sunucudaki rollerini buradan seçebilirsin.",
        components: [{ type: 1, components: [
          { type: 2, style: 1, label: "Rollerimi seç", custom_id: "roller-ac" },
        ] }],
      }).catch(console.error);
    }
    if (t === "INTERACTION_CREATE") onPress(d).catch(console.error);
  },
});
```

```bash
node rol-secici.mjs
```

Bilmen gerekenler:

- Rol istekleri ilk yanıttan **sonra** gider. Bot önce `type: 7` ile cevap verir, böylece 3 saniye kuralı rol isteklerinin süresinden etkilenmez. Bir istek başarısız olursa kişi ayrı bir ephemeral takip mesajı görür.
- Ephemeral mesajı düzenlerken göndermediğin alan silinir. Bu yüzden `type: 7` her seferinde metni, `flags`'i ve menünün tamamını gönderir.
- Menünün işaretli seçenekleri olaydaki `member.roles`'dan gelir, yani kişinin o anki rolleridir. Kişi başka yerden rol alıp bırakırsa bir sonraki basışta doğru görünür.
- Ephemeral mesaj sayfa yenilenince gider. Kişi menüyü kaybederse butona yeniden basar.

## Sayfalı liste

Uzun bir listeyi dörder satırlık sayfalar halinde gösterir. "Önceki" ve "Sonraki" butonları aynı mesajı `type: 7` ile değiştirir. Son basmadan 2 dakika sonra bot iki butonu da kapatır. Kapalı buton soluk görünür ve basılmaz; ekranı henüz güncellenmemiş biri basarsa "Bu bileşen kapalı." görür ve bota bir şey gelmez.

**Gereken izinler:** Kanalları görüntüle, Mesaj gönder. `izin=3072`.

**Gereken intent'ler:** Yok.

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

const { CHANNEL_ID } = process.env;
if (!CHANNEL_ID) throw new Error("CHANNEL_ID tanımlı olmalı");

// Kendi listenle değiştir.
const ITEMS = [
  "Herkese saygılı ol.", "Reklam yapma.", "Spoiler'ı gizle.", "Kanalın konusunda kal.",
  "Aynı mesajı tekrar tekrar atma.", "Kişisel bilgi paylaşma.", "Yetkiliye DM'den yaz.",
  "Ses kanalında müzik çalarken sor.", "Takma adın okunabilir olsun.", "Kurallar herkes için geçerli.",
];
const PER_PAGE = 4;
const PAGES = Math.ceil(ITEMS.length / PER_PAGE);
// Son basmadan bu kadar sonra butonlar kapanır.
const OPEN_FOR = 2 * 60 * 1000;

function view(page, open = true) {
  const start = page * PER_PAGE;
  const lines = ITEMS.slice(start, start + PER_PAGE).map((item, i) => `${start + i + 1}. ${item}`);
  return {
    content: "Sunucu kuralları",
    embeds: [{ description: lines.join("\n"), footer: { text: `Sayfa ${page + 1} / ${PAGES}` } }],
    // Gidilecek sayfa custom_id'nin içinde: bot yeniden başlasa da butonlar çalışır.
    components: [{ type: 1, components: [
      { type: 2, style: 2, label: "Önceki", custom_id: `sayfa:${page - 1}`, disabled: !open || page === 0 },
      { type: 2, style: 2, label: "Sonraki", custom_id: `sayfa:${page + 1}`, disabled: !open || page === PAGES - 1 },
    ] }],
  };
}

// Her mesajın kapanma zamanlayıcısı; her basmada baştan kurulur.
const timers = new Map();

function closeLater(messageId, page) {
  clearTimeout(timers.get(messageId));
  timers.set(messageId, setTimeout(() => {
    timers.delete(messageId);
    // Yalnızca components gönderilir: metin ve embed kalır, mesaj "(düzenlendi)" olmaz.
    call("PATCH", `/channels/${CHANNEL_ID}/messages/${messageId}`, { components: view(page, false).components })
      .catch(console.error);
  }, OPEN_FOR));
}

async function onPress(d) {
  const [kind, page] = d.data.custom_id.split(":");
  if (kind !== "sayfa") return;
  await call("POST", `/interactions/${d.id}/${d.token}/callback`, { type: 7, data: view(Number(page)) });
  closeLater(d.message.id, Number(page));
}

let posted = false;

await connect({
  intents: 0,
  onEvent(t, d) {
    if (t === "READY" && !posted) {
      posted = true;
      call("POST", `/channels/${CHANNEL_ID}/messages`, view(0))
        .then((m) => closeLater(m.id, 0))
        .catch(console.error);
    }
    if (t === "INTERACTION_CREATE") onPress(d).catch(console.error);
  },
});
```

```bash
node sayfali-liste.mjs
```

Bilmen gerekenler:

- Gidilecek sayfa `custom_id`'nin içinde durur, bot ek bir kayıt tutmaz. Sunucu `custom_id`'nin mesajda gerçekten olduğunu doğruladığı için kimse olmayan bir sayfa isteyemez.
- Sayfayı biri değiştirince kanaldaki herkes yeni sayfayı görür. Herkes kendi sayfasında gezinsin istiyorsan listeyi ephemeral bir yanıtla aç; [Rol seçici](https://noluyo.net/gelistirici/ornekler.md#rol-secici) aynı yolu kullanır.
- Butonları kapatan `PATCH` yalnızca `components` gönderir: metin ve embed olduğu gibi kalır, mesaj "(düzenlendi)" olmaz.
- Zamanlayıcılar programın belleğindedir. Bot yeniden başlarsa eski mesajın butonları açık kalır ve çalışmaya devam eder, çünkü sayfa `custom_id`'nin içindedir.

## Bu sayfayı yapay zekaya verirsen

> **İpucu:** Bu sayfayı bir yapay zeka asistanına verip bot yazdırıyorsan şu kuralları da ekle:
>
> - Token koda yazılmaz, `process.env.BOT_TOKEN`'dan okunur.
> - `INTERACTION_CREATE` gelince ilk yanıt 3 saniye içinde gider. Uzun iş (rol vermek, başka bir servise sormak) yanıttan sonra yapılır; gerekirse önce `type: 5` ya da `type: 6` gönderilir.
> - Etkileşim başına tek `callback` gönderilir; sonrası `@original` ya da takip mesajıdır (en çok 5).
> - Gereken bilgi `custom_id`'ye yazılır (en çok 100 karakter, mesajda tekil). Gizli bilgi konmaz: kanalı gören herkesin istemcisine gider.
> - Basan kişinin yetkisi `member.permissions`'tan denetlenir; sunucu bunu senin yerine yapmaz.
> - Ephemeral (`flags: 64`) mesaj dosya almaz; düzenlerken mesajın tamamı gönderilir.
> - Bir mesajda en çok 5 satır, satırda en çok 5 buton ya da tek menü olur. Embed sınırları [Embed'ler](https://noluyo.net/gelistirici/embedler.md) sayfasında.
> - `429` gelince `retry_after` kadar beklenir.

## Sırada ne var

- Botu sürekli çalışır tutarken token'ı nasıl koruyacağın: [Güvenlik](https://noluyo.net/gelistirici/guvenlik.md)
- Bağlantının ayrıntıları: [Gateway](https://noluyo.net/gelistirici/gateway.md)
- Buton, menü ve embed'in ayrıntıları: [Bileşenler](https://noluyo.net/gelistirici/bilesenler.md), [Etkileşimler](https://noluyo.net/gelistirici/etkilesimler.md), [Embed'ler](https://noluyo.net/gelistirici/embedler.md)
- Bir şey ters gidiyorsa: [Sık sorulan sorular](https://noluyo.net/gelistirici/sss.md)
- Başka neler yapılabileceği: [REST referansı](https://noluyo.net/gelistirici/api.md)
