İçeriğe geç
N'oluyo
Rehberler

HTTP ile teslim

Son güncelleme:

Etkileşimleri gateway yerine kendi https adresine aldırabilirsin: N'oluyo her komutu, basmayı ve form gönderimini imzalı bir POST isteğiyle adresine yollar, sen de aynı isteğin yanıtında cevap verirsin.

Hangi modu seçmeliyim

İhtiyacınGatewayHTTP
Bot sürekli çalışan bir programUygunOlur, ama gerek yok
Sunucusuz bir fonksiyon, yalnız istek gelince çalışan bir sunucuOlmaz: gateway açık bir bağlantı isterUygun
Mesaj, üye, tepki gibi olayları dinlemekUygunOlmaz: HTTP'ye yalnızca etkileşimler gelir
Yalnızca komut, buton, menü ve formUygunUygun

Adres kaydettiğin an etkileşimler yalnızca adresine gelir, gateway'e INTERACTION_CREATE gelmez. Olayları da dinlemek istiyorsan gateway bağlantın açık kalır; ikisi birlikte çalışır. Adresi kaldırınca etkileşimler yeniden gateway'e döner.

En küçük çalışan örnek

Node.js 22 veya üstü yeter, ek paket gerekmez. Program 3000 portunda düz HTTP dinler; önüne https sağlayan bir ters vekil (Caddy, nginx gibi) koyarsın, çünkü adres https olmalıdır. /selam komutuna yanıt verir:

http-bot.mjs
import { createServer } from "node:http";
import { createPublicKey, verify } from "node:crypto";

// Portaldaki "Herkese açık anahtar": 64 onaltılık karakter. Gizli değildir, ama koda gömme.
const PUBLIC_KEY = process.env.NOLUYO_PUBLIC_KEY;
const PORT = Number(process.env.PORT ?? 3000);

// Ham 32 baytlık Ed25519 anahtarını Node'un okuduğu SPKI biçimine saran sabit önek.
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
const key = createPublicKey({ key: Buffer.concat([SPKI_PREFIX, Buffer.from(PUBLIC_KEY, "hex")]), format: "der", type: "spki" });

const MAX_AGE = 5 * 60; // saniye
const seen = new Map(); // etkileşim kimliği -> geldiği an

function signatureOk(req, raw) {
  const sig = req.headers["x-signature-ed25519"];
  const ts = req.headers["x-signature-timestamp"];
  if (typeof sig !== "string" || !/^[0-9a-f]{128}$/.test(sig)) return false;
  if (typeof ts !== "string" || !/^\d+$/.test(ts)) return false;
  // Eski bir isteğin yeniden yollanması reddedilir.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > MAX_AGE) return false;
  return verify(null, Buffer.concat([Buffer.from(ts), raw]), key, Buffer.from(sig, "hex"));
}

function answer(i) {
  if (i.type === 1) return { type: 1 }; // PING
  if (i.type === 2 && i.data.name === "selam") {
    const user = i.member?.user ?? i.user;
    return { type: 4, data: { content: `Selam ${user.display_name ?? user.username}!` } };
  }
  return { type: 4, data: { content: "Bunu tanımıyorum.", flags: 64 } };
}

createServer((req, res) => {
  if (req.method !== "POST") return void res.writeHead(405).end();
  const chunks = [];
  let size = 0;
  req.on("data", (c) => {
    size += c.length;
    if (size > 1 << 20) req.destroy();
    else chunks.push(c);
  });
  req.on("end", () => {
    const raw = Buffer.concat(chunks);
    // Önce imza, ham gövdeyle. JSON'a ancak imza tutunca çevrilir.
    if (!signatureOk(req, raw)) return void res.writeHead(401).end("invalid request signature");
    let i;
    try {
      i = JSON.parse(raw);
    } catch {
      return void res.writeHead(400).end();
    }
    // Aynı istek ikinci kez gelirse iş ikinci kez yapılmaz.
    const now = Date.now();
    for (const [id, at] of seen) if (now - at > 2 * MAX_AGE * 1000) seen.delete(id);
    if (seen.has(i.id)) return void res.writeHead(204).end();
    seen.set(i.id, now);
    res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(answer(i)));
  });
}).listen(PORT, () => console.log(`Dinleniyor: ${PORT}`));

Çalıştırmak için:

Bash
NOLUYO_PUBLIC_KEY='portaldaki_acik_anahtar' node http-bot.mjs

/selam komutunu önce Komutlar sayfasındaki gibi kaydet. Sonra adresini portala yaz (aşağıda). Bu programın bot token'ına ihtiyacı yoktur; takip mesajı ya da @original düzenlemesi yapacaksan token'ı process.env.BOT_TOKEN'dan okursun.

Adres kuralları

Adres kaydedilirken ve her bağlantıda şunlar denetlenir. Uymayan adres endpoint_not_allowed alır ve ona hiç istek gitmez.

KuralNeden
Yalnızca https://İstek kişilerin verisini taşır; düz HTTP'de yolda okunabilir.
Port yok ya da 443Başka portlarda çoğunlukla iç servisler durur; N'oluyo bir port tarayıcısına dönmesin.
Kullanıcı adı ve şifre yok (https://ad:sifre@...)Adresin içindeki şifre loglara düşer, ayrıca asıl host'u gizlemenin bilinen bir yoludur.
# yok# sonrası sunucuya hiç gitmez; adresin ne olduğunu karıştırır.
Host bir ad olmalı, IP olmazIP adresi, ada bakan denetimleri atlatır. https://203.0.113.5/ reddedilir.
Host yalnızca küçük-büyük İngilizce harf, rakam, - ve ., en az iki parça (bot.ornek.com)Başka alfabelerden harfler bir adı başka bir ad gibi gösterebilir. localhost tek parçadır.
N'oluyo'nun kendi adresleri olmaz (noluyo.net ve alt alanları)N'oluyo kendini çağırmasın.
En çok 2048 karakterDaha uzunu bir adres değil, veri taşıma girişimidir.
Ad, her bağlantıda genel bir IP'ye çözülmeli10.x, 192.168.x, 127.0.0.1, ::1 gibi yerel ve özel ağ adresleri reddedilir. Denetim DNS'ten sonra her bağlantıda yapılır, kayıttan sonra DNS'i değiştirmek işe yaramaz.
Yönlendirme izlenmez301, 302 gibi bir yanıt başarısız sayılır. Adresin son adres olsun.

Host'u küçük harfe çevirip sonundaki . ve :443'ü atarak kaydederiz. Sorgu dizesi (?anahtar=...) serbesttir ve aynen saklanır: N'oluyo onu loglara yazmaz, adresi yalnızca sen görürsün. İstersen adresine kendi gizli değerini koyup isteğin senden geldiğini bir kez daha denetleyebilirsin; asıl kanıt yine imzadır.

Portalda adım adım

  1. Programını çalıştır ve adresin dışarıdan https ile açıldığını denetle.

  2. Geliştirici portalında uygulamanı aç, Etkileşimler bölümüne in.

  3. Herkese açık anahtar'ı Kopyala ile kopyala ve programına NOLUYO_PUBLIC_KEY olarak ver. Anahtar her uygulamaya özeldir ve hiç değişmez.

  4. Etkileşim adresi alanına adresini yaz, Kaydet ve test et'e bas.

  5. N'oluyo adresine iki PING yollar (aşağıda). İkisi de doğru yanıtlanırsa adres kaydedilir ve Kaydedildi görürsün; o andan itibaren etkileşimler buraya gelir. Test geçmezse hiçbir şey kaydedilmez ve ne yapman gerektiği yazılır: örneğin imza denetimi yoksa "Geçersiz imzaya 401 dönmüyor. İmza denetimini ekle."

  6. Gateway'e dönmek için Adresi kaldır'a bas.

Aynı işi otomasyonla yapmak için Uygulamanı güncelle rotasına interactions_endpoint_url gönder; null adresi kaldırır.

İmzayı doğrula

Her isteğin iki başlığı vardır:

Başlıkİçeriği
X-Signature-Ed25519İmza, 128 onaltılık karakter
X-Signature-Timestampİsteğin yollandığı an, Unix saniyesi

İmza, Ed25519 ile, X-Signature-Timestamp'in metni ve ham gövdenin baytları art arda eklenerek atılır. Doğrulamak için:

  1. Gövdeyi bayt olarak oku. Ham gövdeyi JSON'a çevirmeden doğrula: çevirip yeniden yazarsan boşluklar ve alan sırası değişir, imza tutmaz.

  2. Zaman damgası şimdiden 5 dakikadan eskiyse ya da ilerideyse reddet. Böylece yakalanmış eski bir istek yeniden yollanamaz.

  3. timestamp + gövde üzerinde imzayı uygulamanın açık anahtarıyla doğrula.

  4. Herhangi biri tutmazsa 401 dön ve hiçbir şey yapma.

Python'da doğrulama, standart kütüphanede Ed25519 olmadığı için ek bir paket ister: pip install cryptography.

python
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import os, time

KEY = Ed25519PublicKey.from_public_bytes(bytes.fromhex(os.environ["NOLUYO_PUBLIC_KEY"]))

def signature_ok(headers, raw: bytes) -> bool:
    sig, ts = headers.get("X-Signature-Ed25519", ""), headers.get("X-Signature-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    try:
        KEY.verify(bytes.fromhex(sig), ts.encode() + raw)
        return True
    except (InvalidSignature, ValueError):
        return False

PING ve iki istekli test

Adresi kaydederken N'oluyo şu gövdeyi iki kez yollar:

JSON
{ "id": "112794120000000000", "application_id": "112793931040686080", "type": 1, "token": "", "version": 1 }
İstekBeklenen yanıt
Doğru imzalı PING200 ve gövdede {"type":1}
Yanlış imzalı PING401

Neden iki istek: ilki adresin gerçekten bu botun programı olduğunu gösterir, çünkü yalnızca senin programın PING'e {"type":1} döner; böylece kimse N'oluyo'yu başkasının adresine yönlendiremez. İkincisi programın imzayı gerçekten denetlediğini gösterir. Test kişiyi bekletir: iki istek birlikte en çok 6 saniye kadar sürer ve kişi başına dakikada en çok 5 test yapılır.

Bot çalışırken de arada bir PING gelebilir; her zaman {"type":1} dön.

Yanıt ver

İstek, gateway'deki INTERACTION_CREATE olayının d alanıyla aynı nesnedir; alanlar Etkileşimler ve Komutlar sayfalarında. Yanıtın üç biçimi vardır:

DurumGövdeAnlamı
200JSON: { "type": ..., "data": ... }Yanıtın kendisi, callback rotasına gönderdiğinle aynı biçim: 4, 5, 6, 7, otomatik tamamlamada 8, formda 9.
202 ya da 204Boş"Yanıtı callback rotasıyla vereceğim." Bu bir yanıt sayılmaz; 3 saniye kuralı sürer, callback'i yine zamanında göndermelisin.
Başka her şeyBaşarısız: kişi botun yanıt vermediğini görür.

Kişinin isteği sen yanıt verene kadar açık bekler, en çok 3 saniye. 200 gövdesi tek bir JSON nesnesidir; tanımadığımız alanlar yok sayılır. Gövde, callback rotasına göndereceğin yanıtla aynı denetimlerden ve sınırlardan geçer. Tek fark: reddedilirse hata kodunu sen görmezsin, kişi botun yanıt vermediğini görür ve kod teslim günlüğüne yazılır.

200 yanıtı dosya taşıyamaz, çünkü JSON'dur. Görselli yanıt için 5 dön, sonra görseli @original düzenlemesiyle ya da takip mesajıyla yolla. Yanıt en çok 64 KiB olabilir.

200 ile mesaj yazan bir yanıt da gateway'den gelen botların sınırlarına sayılır: aynı yazma tavanı, aynı yayılma bütçesi, aynı interaction sınıfı. Yoğunlukta kişi interaction_busy görür; bu senin botunun suçu olmayabilir, tüm botların ortak tavanı dolu olabilir. HTTP, sınırlardan kaçış yolu değildir. Ayrıntı Hız sınırları sayfasında.

Her tür için gövde

Ne istiyorsun200 gövdesi
Komuta ya da basmaya yeni mesajla yanıt{"type":4,"data":{"content":"Selam!"}}
Yalnızca kişi görsün{"type":4,"data":{"content":"Yalnızca sen görüyorsun.","flags":64}}
Uzun iş: önce "düşünüyor"{"type":5}
Butona sessiz onay{"type":6}
Basılan mesajı güncelle{"type":7,"data":{"content":"Sayaç: 2"}}
Otomatik tamamlama önerileri{"type":8,"data":{"choices":[{"name":"Ankara","value":"ankara"}]}}
Form aç{"type":9,"data":{"custom_id":"sikayet","title":"Şikayet","components":[{"type":1,"components":[{"type":4,"custom_id":"konu","style":1,"label":"Konu"}]}]}}

Hangi etkileşimin hangi türü aldığı Etkileşimler sayfasında. Gönderilen form da adresine yeni bir etkileşim (type: 5) olarak gelir.

Uzun iş: önce 5, sonra @original

3 saniyeye sığmayacak işte hemen {"type":5} dön, işi yanıttan sonra bitir ve mesajı bot token'ıyla doldur. Token'ın ömrü 15 dakikadır. Yukarıdaki http-bot.mjs'te answer işlevini bununla değiştir:

http-bot.mjs
const API = "https://noluyo.net/api/v1";

// Senin uzun işin; burada 5 saniye bekliyor.
async function makeReport() {
  await new Promise((done) => setTimeout(done, 5000));
  return "Rapor hazır.";
}

async function finish(i) {
  const content = await makeReport();
  const res = await fetch(`${API}/interactions/${i.id}/${i.token}/messages/@original`, {
    method: "PATCH",
    headers: { Authorization: `Bot ${process.env.BOT_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ content }),
  });
  if (!res.ok) console.error("Doldurulamadı:", res.status, await res.text());
}

function answer(i) {
  if (i.type === 1) return { type: 1 }; // PING
  if (i.type === 2 && i.data.name === "rapor") {
    finish(i).catch(console.error); // yanıt gittikten sonra sürer
    return { type: 5 };
  }
  if (i.type === 2 && i.data.name === "selam") {
    const user = i.member?.user ?? i.user;
    return { type: 4, data: { content: `Selam ${user.display_name ?? user.username}!` } };
  }
  return { type: 4, data: { content: "Bunu tanımıyorum.", flags: 64 } };
}

Çalıştırmak için token'ı da ver:

Bash
NOLUYO_PUBLIC_KEY='portaldaki_acik_anahtar' BOT_TOKEN='bot_tokenin' node http-bot.mjs

202 ile, callback rotasından

İstersen yanıtı gövdede değil, gateway'deki gibi callback rotasıyla ver: hemen 202 dön, sonra 3 saniye içinde rotayı çağır. Bu, yanıtı başka bir süreçte hazırlayan botlar içindir; tek süreçte gövdeyle yanıt vermek daha kısadır.

JavaScript
// createServer içinde, imza tuttuktan sonra, answer() yerine:
res.writeHead(202).end();
fetch(`${API}/interactions/${i.id}/${i.token}/callback`, {
  method: "POST",
  headers: { Authorization: `Bot ${process.env.BOT_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ type: 4, data: { content: "Selam!" } }),
}).catch(console.error);

Kişi ne görür

DurumKişinin isteğiKişi ne görür
200 ve yanıt geçti200Botun yanıtı
202, 204200callback gelene dek bekler; 3 saniyede gelmezse "Bot yanıt vermedi"
Yanıt gelmedi, reddedildi ya da devre kesici açık504 application_no_response"Bot zamanında yanıt vermedi."
Adresine aynı anda 16 teslim gidiyor ya da tüm botların tavanı dolu429 interaction_busy"Bot şu an yoğun. Biraz sonra yeniden dene."

Kişi beklerken sayfayı kapatsa da yanıtın işlenir.

Zaman, tekrar ve tekilleştirme

  • 3 saniye. Bağlanma, TLS ve yanıtın tamamı 3 saniyeyi geçerse istek başarısız sayılır. Uzun iş yapacaksan önce 5 dön, işi sonra bitir.

  • Tekrar deneme yok. Kişi beklediği için her etkileşim adresine en çok bir kez yollanır. Başarısız olursa kişi hatayı hemen görür ve isterse yeniden dener; bu yeni bir etkileşimdir.

  • Kimliğe göre tekilleştir. İmzalı bir isteği yakalayan biri onu 5 dakika içinde yeniden yollayabilir. Gördüğün id'leri en az 10 dakika tut ve aynısı gelirse işi ikinci kez yapma.

  • Aynı anda en çok 16 istek. Bir uygulamanın adresine aynı anda en çok 16 teslim gider; dolarsa kişi hemen interaction_busy görür.

Devre kesici ve teslim günlüğü

Adresin art arda 10 kez başarısız olursa N'oluyo onu 30 saniye aramaz (portal günlüğün üstünde "Adres yanıt vermediği için teslim 30 saniye durdu." yazar); bu sürede kişiler botun yanıt vermediğini hemen görür. Sonra 30 saniyede bir tek bir deneme yapar, biri başarılı olunca normale döner. Yalnızca adresinin kendi hataları sayılır: timeout, dns, connect, tls, redirect, status, too_large, not_allowed ve bad_json. Adresinin yanıt verdiği ama yanıtın uygulanırken reddedildiği teslimler (busy, error ve hata kodları, örneğin kişi DM'yi kapattığı için dm_closed) günlükte görünür, ama bu sayıma girmez ve sayımı sıfırlamaz: onlar adresinin değil, kişinin ya da tavanın sonucudur. Yeni bir adres kaydedince sayım ve günlük sıfırlanır.

Portalda Etkileşimler bölümündeki Teslim günlüğü (Yenile ile tazelenir), adresine yapılan son 50 teslimi en yenisi önce gösterir. Günlük bellekte tutulur, N'oluyo yeniden başlarsa boşalır. Kişi ve içerik yoktur:

SütunAnlamı
ZamanTeslimin anı
TürEtkileşimin türü: 2 komut, 3 bileşen, 4 otomatik tamamlama, 5 form
DurumAdresinin HTTP durumu, hiç yanıt gelmediyse 0
SüreMilisaniye
SonuçAşağıdaki tablo
SonuçNe olduNe yap
okBaşarılı
timeout3 saniyede yanıt gelmediİşi kısalt ya da önce 5 dön.
dnsAd çözülemediDNS kaydını denetle.
connectBağlanılamadıSunucun ve 443 portu açık mı bak.
tlsTLS kurulamadıSertifikan geçerli ve adla eşleşiyor mu bak.
redirect3xx döndüSon adresi kaydet.
status200, 202 ve 204 dışında bir durumProgramının loguna bak; geçersiz imzaya 401 doğrudur, geçerliye değil.
too_largeYanıt 64 KiB'tan büyükYanıtı küçült.
not_allowedAd yerel ya da özel bir IP'ye çözüldüAdın genel bir IP göstermeli.
bad_json200 geldi ama gövde tek bir JSON nesnesi değilGövdeyi JSON.stringify ile yaz, ardına bir şey ekleme.
busyYanıt geldi ama tüm botların ortak tavanı doluydu; kişi interaction_busy gördüBir şey yapma; sürerse Hız sınırları sayfasına bak.
errorBizim tarafımızda bir hataSürerse bize bildir.
Bir hata koduYanıt geldi ama reddedildi; callback rotası da aynı kodu dönerdi: interaction_response_invalid, validation_failed, dm_closed, application_unavailable gibiKodun anlamı Hatalar sayfasında.

Aynı bilgiyi Teslim günlüğünü oku rotası da verir; open: true devre kesicinin açık olduğunu söyler.

Yerelde nasıl denersin

https://localhost:3000 kaydedilemez: N'oluyo'nun sunucuları senin bilgisayarına ulaşamaz, yerel adresler de zaten reddedilir. Denemek için şu yollardan birini seç:

  • Bilgisayarındaki 3000 portunu genel bir https adrese açan bir tünel servisi kullan ve onun verdiği adresi kaydet. İşin bitince adresi kaldır.

  • Programı ucuz bir sunucuya ya da sunucusuz bir fonksiyona koy.

  • Kayıttan önce imza kodunu kendi testinle dene: yanlış imzaya 401, doğru PING'e {"type":1} dönmeli.

Limitler

AnahtarDeğerSaniyeAnlamı
verify560Kişi başına adres testi
timeout3Bir teslimin tamamı
response65536Yanıtın en büyük boyutu, bayt
in_flight16Bir uygulamaya aynı anda giden teslim
trip1030Bu kadar ardışık hatadan sonra adres bu kadar saniye aranmaz
freshness300Kabul edeceğin en eski zaman damgası, saniye
url2048Adresin en çok karakteri

Hatalar

Adres kaydedilirken dönebilecek hatalar. Yalnızca hatanın türü söylenir; adresinin döndüğü durum ya da metin söylenmez.

KodNe olduNe yap
endpoint_not_allowedAdres kurallara uymuyor ya da ad yerel bir IP'ye çözülüyorYukarıdaki tabloya bak.
endpoint_unreachableAdrese ulaşılamadı ya da 3 saniyede yanıt gelmediProgramın açık mı, https ve sertifika doğru mu bak.
endpoint_bad_responseDoğru imzalı PING'e 200 {"type":1} dönmeditype: 1 gelince {"type":1} dön; yönlendirme yapma.
endpoint_no_signature_checkYanlış imzalı isteğe 401 dönmediİmzayı her istekte doğrula, tutmazsa 401 dön.
rate_limitedDakikada 5 testi aştınretry_after kadar bekle.

Bu sayfayı yapay zekaya verirsen

Sırada ne var