İçeriğe geç
N'oluyo
Gateway

Gateway

Son güncelleme:

REST'te programın sorar, N'oluyo yanıtlar. Yeni bir mesaj yazıldığını öğrenmek için sormak gerekseydi kanalı durmadan okuman gerekirdi. Gateway bunu tersine çevirir: programın açık duran tek bir WebSocket bağlantısı kurar, sunucularda bir şey olduğunda haber kendiliğinden gelir.

Botun iki yolu birlikte kullanır: gateway'den duyar, REST ile iş yapar. Mesaj göndermek, silmek ya da rol vermek yine REST isteğidir.

Bu sayfa bir bağlantının başından sonuna ne olduğunu sırayla anlatır. Sayfanın sonunda kopyalayıp kullanabileceğin bir istemci var. Her olayın alanları ve örnekleri Olaylar sayfasında.

Akışa kısa bakış

  1. GET /gateway/bot ile bağlanacağın adresi öğrenirsin.

  2. O adrese WebSocket açarsın. İlk gelen çerçeve op 10 (HELLO) olur ve heartbeat aralığını söyler.

  3. O aralıkla op 1 (heartbeat) göndermeye başlarsın.

  4. op 2 (identify) ile kim olduğunu ve hangi olayları istediğini söylersin.

  5. READY gelir, ardından sunucuların tek tek GUILD_CREATE olarak akar.

  6. Olaylar gelmeye başlar. Her olayın sıra numarasını saklarsın.

  7. Bağlantı koparsa yeniden bağlanıp op 6 (resume) ile kaldığın yerden sürersin.

Çerçeveler

Her iki yönde de her mesaj, metin olarak yazılmış tek bir JSON nesnesidir:

JSON
{ "op": 0, "d": { "id": "21531879014400001" }, "s": 42, "t": "GUILD_DELETE" }
AlanAnlamı
opÇerçevenin ne olduğu. Tüm opcode'lar Olaylar sayfasında.
dİçerik. Biçimi op değerine ve olaya göre değişir.
sOlayın sıra numarası. Yalnızca op 0 çerçevelerinde dolu olur.
tOlayın adı, örneğin MESSAGE_CREATE. Yalnızca op 0 çerçevelerinde dolu olur.

Senin gönderdiğin bir çerçeve en çok 16 KiB olabilir ve dakikada en çok 120 çerçeve gönderebilirsin, heartbeat'ler dahil. Bu sayıyı aşan bağlantı 4008 ile kapanır.

Sıkıştırma botlar için yok. Bağlantı adresine compress eklesen de bir şey değişmez; zstd botlara sunulmaz. Çerçeveler her zaman düz JSON metnidir, kod çözücü gerekmez.

1. Adresi al

Botlar kendi adreslerine bağlanır. Bu adresi koda yazma, her başlangıçta GET /gateway/bot ile sor:

Bash
curl "https://noluyo.net/api/v1/gateway/bot" \
  -H "Authorization: Bot $BOT_TOKEN"
JSON
{
  "url": "wss://noluyo.net/bot-gateway",
  "shards": 1,
  "session_start_limit": {
    "total": 1000,
    "remaining": 998,
    "reset_after": 41230000,
    "max_concurrency": 1
  }
}
  • url: Bağlanacağın adres.

  • shards: Kaç bağlantı açmanın önerildiği. Çoğu bot için 1; ayrıntısı Shard'lar sayfasında.

  • session_start_limit: Bugün kaç oturum başlatabileceğin ve sayacın kaç milisaniye sonra sıfırlanacağı. Bu rotayı okumak oturum hakkı harcamaz.

2. Bağlan ve HELLO'yu bekle

JavaScript
const ws = new WebSocket(url);

Bağlantı açılınca sunucu hemen op 10 gönderir:

JSON
{ "op": 10, "d": { "heartbeat_interval": 41250 }, "s": null, "t": null }

Bundan sonra 10 saniye içinde identify ya da resume göndermelisin, yoksa bağlantı 4003 ile kapanır. Aynı IP adresinden, henüz identify etmemiş en çok 20 bağlantı açık kalabilir; fazlası 429 ile reddedilir.

3. Heartbeat gönder

heartbeat_interval, milisaniye cinsinden aralıktır. Bu aralıkla op 1 gönder. d alanına son gördüğün s değerini koyabilirsin, sunucu onu okumaz:

JavaScript
setInterval(() => ws.send(JSON.stringify({ op: 1, d: seq })), heartbeat_interval);

Sunucu her heartbeat'e op 11 ile yanıt verir. İki durumu bilmen gerekir:

  • Yanıt gelmezse bağlantı ölmüş demektir, kapanmış gibi görünmese de. Bir sonraki heartbeat zamanı geldiğinde hâlâ yanıt yoksa bağlantıyı bırak, yeniden bağlan ve resume yap.

  • Sen göndermezsen sunucu aralığın bir buçuk katı kadar bekler, sonra bağlantıyı kapatır. Bu kapanış bir kod taşımaz, sen 1006 görürsün. Oturum sürdürülebilir kalır: yeniden bağlanıp resume yap.

4. Identify gönder

Identify, oturumu başlatan çerçevedir:

JSON
{
  "op": 2,
  "d": {
    "token": "<token>",
    "intents": 33281,
    "shard": [0, 1],
    "presence": { "status": "online", "activities": [] }
  }
}
AlanGerekli miAnlamı
tokenEvetBotun token'ı. REST'teki gibi Bot öneki almaz.
intentsEvetHangi olayları istediğin, bitlerin toplamı olarak. Buradaki 33281: GUILDS + GUILD_MESSAGES + MESSAGE_CONTENT. Bkz. Intent'ler.
shardHayır[id, count]. Verilmezse [0, 1] olur. Bkz. Shard'lar.
presenceHayırBotun başlangıç durumu: online, idle, dnd ya da invisible.

Her başarılı identify günlük oturum haklarından birini harcar, reddedilen identify harcamaz. Oturumlar en çok 5 saniyede bir başlatılabilir; daha sık denersen bağlantı 4008 ile kapanır.

5. READY ve GUILD_CREATE

Identify kabul edilince ilk olay READY gelir:

JSON
{
  "op": 0,
  "s": 1,
  "t": "READY",
  "d": {
    "v": 1,
    "user": { "id": "21879171579904005", "username": "selam_botu", "bot": true },
    "session_id": "6f1c0e2ab7d94c3f8e5a1d0b9c7e4a26",
    "application": { "id": "21879167385600004" },
    "shard": [0, 1],
    "guilds": [{ "id": "21531879014400001", "unavailable": true }]
  }
}

session_id değerini sakla, resume için gerekir. user.id de işine yarar: bot kendi mesajlarını da duyar ve onlara yanıt vermemek için bu ID ile karşılaştırman gerekir.

guilds listesi yalnızca ID'leri verir ve hepsi unavailable görünür. Bu, sunucuların kapalı olduğu anlamına gelmez, henüz gönderilmedikleri anlamına gelir. Asıl içerikleri ardından tek tek GUILD_CREATE olarak gelir, senin okuma hızına göre. Yani READY yalnızca neyin geleceğini söyler: listedeki her sunucu için bir GUILD_CREATE bekle.

Bu akışta üç şey yeni başlayanları şaşırtır:

  • Bir sunucunun olayları, sırası gelene kadar gelmez. Sıra ona yaklaşınca başlar. Yani READY'den hemen sonra her sunucuyu dinliyor sayılmazsın; o arada yazılan mesajları görmek istersen REST ile okumalısın.

  • Olay, GUILD_CREATE'ten önce gelebilir. Bir sunucunun ilk olayı onun GUILD_CREATE'inden hemen önce ulaşabilir. Kodun, tanımadığı bir guild_id gördüğünde hata vermemeli.

  • GUILD_CREATE, aldığın bir olaydan eski olabilir. Sunucu önce olayları sana açar, sonra sunucunun o anki halini okur, sonra gönderir. Bu arada olan bir değişiklik önce olay olarak gelir, sonra gelen GUILD_CREATE onu içermeyebilir. Bazen de tersi olur: aynı değişiklik hem anlık görüntüde hem olayda bulunur.

Sunucuların bilgisini bellekte tutuyorsan güvenli yol şudur: Bir sunucu için GUILD_CREATE'ten önce gelen olayları kenara yaz. GUILD_CREATE gelince bilgiyi ondan kur, sonra kenardaki olayları sırayla uygula. Kodunu, aynı olay iki kez uygulansa da bir şey bozulmayacak biçimde yaz: "ekle" yerine "yoksa ekle, varsa güncelle" mantığıyla.

Bot sonradan bir sunucuya eklenirse aynı GUILD_CREATE o an gelir. Çıkarılırsa ya da sunucu silinirse GUILD_DELETE gelir. Bu, READY'de olup henüz gönderilmemiş bir sunucu için de geçerlidir; bu yüzden tüm sunucuları beklerken GUILD_DELETE'i de o sunucunun cevabı say. Aynı sunucu için iki kez gelebilir, ikincisi zarar vermeyecek şekilde yaz. GUILD_CREATE üye olarak yalnızca botu listeler; diğer üyeler için aşağıdaki op 8 var. GUILD_VOICE_STATES istemediysen voice_states boştur.

6. Olayları dinle ve sıra numarasını sakla

Her op 0 çerçevesindeki s değerini sakla, her zaman en son gördüğünü tut. Resume bunu ister.

JavaScript
ws.onmessage = (e) => {
  const { op, d, s, t } = JSON.parse(e.data);
  if (s) seq = s;
  if (op === 0 && t === "MESSAGE_CREATE") console.log(d.author.username, d.content);
};

Hangi olayların geldiği Intent'ler ile seçtiklerine bağlıdır. MESSAGE_CREATE'te content alanının boş gelmesi hata değildir, aynı sayfadaki mesaj içeriği kuralına bak.

7. Bağlantı koparsa resume yap

Bağlantılar kopar: ağ değişir, sunucu yeniden başlar, programın yavaş kalır. Olay kaçırmamak için yeniden identify etmek yerine oturumu sürdür. Yeni bir bağlantı aç, HELLO'yu bekle, identify yerine şunu gönder:

JSON
{
  "op": 6,
  "d": {
    "token": "<token>",
    "session_id": "6f1c0e2ab7d94c3f8e5a1d0b9c7e4a26",
    "seq": 42
  }
}

Kaçırdığın olaylar sırayla gelir, ardından RESUMED gelir. Resume, oturum hakkı harcamaz. Heartbeat'i yeni bağlantının HELLO'sundaki aralıkla yeniden başlat.

Resume'un sınırları var:

  • Oturum, bağlantı koptuktan sonra 2 dakika bekler. Daha geç kalırsan op 9 gelir.

  • Sunucu oturum başına son 1000 olayı, en çok 2 MiB olacak şekilde saklar. Daha çoğunu kaçırdıysan yine op 9 gelir.

  • Oturum, identify ettiği intent'lerle sürer. Portalda intent açtıysan resume yetmez, yeni identify gerekir.

  • Görmediğin GUILD_CREATE'ler saklanan kopyadan değil, sunucunun o anki halinden yeniden gönderilir. GUILD_MEMBERS_CHUNK ve RATE_LIMITED ise hiç gönderilmez; üye listesini yeniden iste.

Kendin kapatırken kapanış koduna dikkat et: ws.close(1000) oturumu bitirir ve bot çevrimdışı olur. Başka kütüphaneler 1001 de gönderebilir, sunucu onu da aynı sayar. Sonra resume etmek istiyorsan başka bir kod kullan, örneğin ws.close(4000).

op 7 ve op 9

op 7 (RECONNECT): Sunucu kapanmak üzere. Bağlantıyı kendin kapat, yeniden bağlan, resume yap.

op 9 (INVALID_SESSION): Oturum sürdürülemiyor. d her zaman false olur. Sakladığın session_id ve seq değerlerini unut, bellekteki sunucu bilgilerini de eskimiş say. Bağlantı açık kalır: birkaç saniye bekle ve aynı bağlantıda op 2 gönder. Hemen denersen 5 saniyelik sınıra takılabilirsin.

Kapanış kodları

Bağlantı kapanınca onclose içindeki e.code nedenini söyler. Kararını şu tabloya göre ver. Adları ve daha uzun açıklamaları Olaylar sayfasında.

KodAnlamıYeniden bağlanNe yapmalı
4000Sunucuda bir hata olduEvetResume dene.
4001Bilinmeyen ya da botlara kapalı opcodeHayırKodunu düzelt.
4002Çerçeve okunamadı ya da değerleri geçersizHayırKodunu düzelt.
4003Identify'dan önce heartbeat dışında bir şey gönderildi ya da 10 saniye geçtiHayırSırayı düzelt.
4004Token yanlış ya da yenilendi, veya bot ya da sahibi kapatıldıHayırYeni token al, programı yeniden başlat.
4005Oturumu olan bağlantıda yeniden identify ya da resumeHayırKodunu düzelt.
4008Çok fazla çerçeve ya da çok sık oturum başlatmaEvet5 saniye bekle, gönderimini azalt.
4010Platform bakımdaEvetBir süre bekleyip identify dene.
4011Daha yeni bir oturum bunun yerini aldıHayırBotun iki kopyası çalışıyor olabilir, birini durdur.
4012shard yanlış ya da üst sınırın üstündeHayırshard değerini düzelt.
4013intents yok, sayı değil ya da tanımsız bit içeriyorHayırintents değerini düzelt.
4014İzni olmayan ayrıcalıklı intentHayırPortaldan aç ya da istemeyi bırak.
4015Bu sayı için fazla sunucuHayırDaha çok shard ile bağlan.
4016Günlük oturum hakların bittiEvetreset_after kadar bekle.

Kapanış kodu olmadan kopan bağlantılar (1006 gibi) ağ sorunudur ya da sunucu heartbeat'ini zamanında alamamıştır; 1012 (*Service Restart*) ise N'oluyo sunucusu güncellenirken gönderilir ve oturumun sunucu belleğinde korunduğunu gösterir: her ikisinde de session_id ile seq değerlerini koruyup ~1,5 saniye sonra yeniden bağlanarak op 6 (RESUME) gönder. "Hayır" yazan kodlarda döngüye girme, aynı istek aynı sonucu verir.

Botun durumunu değiştirmek: op 3

JSON
{ "op": 3, "d": { "status": "idle", "activities": [] } }

status, online, idle, dnd ya da invisible olabilir. Bir oturum dakikada 5 kez gönderebilir. Değişiklik, botun tüm shard'lardaki sunucularında görünür.

Ses kanalına girmek: op 4

JSON
{ "op": 4, "d": { "guild_id": "21531879014400001", "channel_id": "21531879014400099", "self_mute": false } }

Botu bir ses kanalına sokar; channel_id: null gönderirsen kanaldan çıkarır. Yanıt olarak aynı oturuma LiveKit adresini ve 10 dakikalık token'ı taşıyan VOICE_SERVER_UPDATE, bir kural ya da sınır engellerse VOICE_JOIN_ERROR gelir. Bot seste yalnızca yayın yapar, kimseyi duyamaz. Çalışan örnek ve sınırlar Ses sayfasında.

Üye listesi: op 8

Binlerce üyeyi her bota peşin göndermek israf olurdu, bu yüzden GUILD_CREATE üye listesi taşımaz. Üye listesine ihtiyacı olan bot onu op 8 ile ister:

JSON
{ "op": 8, "d": { "guild_id": "21531879014400001", "query": "ay", "limit": 10, "nonce": "arama-1" } }

query ve user_ids alanlarından tam olarak biri gönderilir:

İstekNe dönerNe ister
"query": "ay"Adı bu sözcükle başlayanlar. limit en çok 100, verilmezse 100.Bir şey istemez.
"user_ids": ["..."]Bu kullanıcılar, en çok 100 ID. Üye olmayanlar not_found içinde.Bir şey istemez.
"query": ""Tam liste. limit verilirse en çok o kadar.GUILD_MEMBERS intent'i

"presences": true üyelerin durumlarını da ekler ve GUILD_PRESENCES ister. Gereken intent oturumda yoksa bağlantı 4014 ile kapanır. nonce, en çok 32 baytlık bir etikettir; yanıtta aynen geri gelir ve hangi isteğin yanıtı olduğunu anlamanı sağlar. query en çok 100 karakter olabilir. Bu sınırları ya da tablodakileri aşan istek bağlantıyı 4002 ile kapatır.

Yanıt, her biri en çok 1000 üye taşıyan GUILD_MEMBERS_CHUNK parçalarıyla gelir. Bitişi şöyle anlarsın:

JavaScript
if (t === "GUILD_MEMBERS_CHUNK") {
  members.push(...d.members);
  if (d.chunk_index + 1 === d.chunk_count) console.log("liste bitti", members.length);
}
  • Geçerli olan, son parçadaki chunk_count değeridir. İlk parçalardaki sayı bir tahmindir: liste gönderilirken üyeler ayrılabilir, o zaman son parça daha erken gelir ve kendi chunk_count değeriyle bittiğini söyler. Sayıyı başta saklayıp ona güvenme, her parçada yeniden bak.

  • Boş bir son parça, listenin eksik olabileceği anlamına gelir. Liste yarıda hata alırsa, boşuna beklemeyesin diye members alanı boş bir son parça gelir. Ayrı bir hata alanı yoktur. Emin olmak için sınırlara uyarak listeyi yeniden iste.

  • Botun içinde olmadığı ya da başka shard'daki bir sunucu için tek bir boş parça gelir.

Aynı anda tek istek yanıtlanır. Bir oturum dakikada 10 istek gönderebilir, tam liste ise sunucu başına 30 saniyede bir istenebilir. Buna uymayan istek bağlantıyı kapatmaz, RATE_LIMITED ile yanıtlanır.

RATE_LIMITED

op 3, op 4 ya da op 8 sınıra takılınca bağlantı kapanmaz, sana bir olay gelir:

JSON
{ "op": 0, "s": 57, "t": "RATE_LIMITED", "d": { "opcode": 8, "retry_after": 12.5, "meta": { "guild_id": "21531879014400001", "nonce": "arama-1" } } }

retry_after saniye kadar bekleyip aynı çerçeveyi yeniden gönder. Sayılar Hız sınırları sayfasında.

INTERACTION_CREATE

Biri botunun mesajındaki butona bastığında ya da menüden seçtiğinde gelir. Intent istemez, yalnızca sunucunun shard'ına gider; DM'den gelirse shard 0'a. Bot çevrimdışıysa olay kaybolur. Ayrıntı Bileşenler sayfasında.

Tam bir istemci

Aşağıdaki dosya bu sayfadaki bağlantı işlerinin hepsini yapar: adresi sorar, heartbeat gönderir, kopunca resume yapar, op 7 ve op 9'u işler, kapanış koduna göre karar verir. Örnekler sayfasındaki botlar bu dosyayı kullanır. gateway.mjs adıyla kaydet:

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

// Bu kodlarda yeniden bağlanmak bir şey düzeltmez.
const FATAL = [4001, 4002, 4003, 4004, 4005, 4011, 4012, 4013, 4014, 4015];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function gatewayInfo() {
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fetch(`${API}/gateway/bot`, {
        headers: { Authorization: `Bot ${BOT_TOKEN}` },
      });
      if (res.status >= 500 && attempt < 5) {
        await sleep(Math.min(10000, 1000 * 2 ** attempt));
        continue;
      }
      if (!res.ok) throw new Error(`GET /gateway/bot: ${res.status}`);
      return res.json();
    } catch (err) {
      if (attempt >= 5 || String(err.message).startsWith("GET /gateway/bot: 4")) throw err;
      await sleep(Math.min(10000, 1000 * 2 ** attempt));
    }
  }
}

export async function connect({ intents, shard, onEvent }) {
  const { url } = await gatewayInfo();
  let sessionId = null;
  let seq = null;
  let current = null;
  let failures = 0;

  function open() {
    const ws = new WebSocket(url);
    current = ws;
    const send = (op, d) => {
      if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op, d }));
    };
    const identify = () => send(2, { token: BOT_TOKEN, intents, shard });
    let heartbeat;
    let reidentifyTimer;
    let acked = true;

    const clearTimers = () => {
      clearInterval(heartbeat);
      clearTimeout(reidentifyTimer);
    };

    // Bu bağlantıyı bırakır. 4000 ile kapattığımız için oturum sürdürülebilir kalır.
    function reopen() {
      clearTimers();
      ws.onmessage = ws.onclose = ws.onerror = null;
      try {
        if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close(4000);
      } catch {}
      setTimeout(open, 1000);
    }

    ws.onerror = () => {};

    ws.onmessage = (e) => {
      const { op, d, s, t } = JSON.parse(e.data);
      if (s) seq = s;
      if (op === 0) {
        if (t === "READY") sessionId = d.session_id;
        onEvent(t, d);
      } else if (op === 10) {
        failures = 0;
        heartbeat = setInterval(() => {
          // Önceki heartbeat yanıtsız kaldı: bağlantı ölmüş.
          if (!acked) return reopen();
          acked = false;
          send(1, seq);
        }, d.heartbeat_interval);
        if (sessionId) send(6, { token: BOT_TOKEN, session_id: sessionId, seq });
        else identify();
      } else if (op === 11) {
        acked = true;
      } else if (op === 7) {
        reopen();
      } else if (op === 9) {
        sessionId = seq = null;
        clearTimeout(reidentifyTimer);
        reidentifyTimer = setTimeout(identify, 5000);
      }
    };

    ws.onclose = async (e) => {
      clearTimers();
      console.error("gateway kapandı", e.code, e.reason);
      if (FATAL.includes(e.code)) process.exit(1);
      if (e.code === 4010) sessionId = seq = null;
      let wait = e.code === 1012 ? 1500 : Math.min(15000, 1000 * 2 ** Math.min(failures++, 4));
      if (e.code === 4008) wait = Math.max(5000, wait);
      if (e.code === 4016) {
        try {
          wait = (await gatewayInfo()).session_start_limit.reset_after;
        } catch {
          wait = 60000;
        }
      }
      setTimeout(open, wait);
    };
  }

  open();
  return {
    send: (op, d) => {
      if (current?.readyState === WebSocket.OPEN) current.send(JSON.stringify({ op, d }));
    },
  };
}

Kullanımı şöyle:

JavaScript
import { connect } from "./gateway.mjs";

await connect({
  intents: (1 << 0) | (1 << 9),
  onEvent(t, d) {
    if (t === "READY") console.log(d.user.username, "bağlandı");
  },
});

onEvent içinde fırlatılan bir hata programı durdurur, kendi işlerini try içinde yap.

Bu sayfayı yapay zekaya verirsen

Sırada ne var