HTTP ile teslim
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ın | Gateway | HTTP |
|---|---|---|
| Bot sürekli çalışan bir program | Uygun | Olur, ama gerek yok |
| Sunucusuz bir fonksiyon, yalnız istek gelince çalışan bir sunucu | Olmaz: gateway açık bir bağlantı ister | Uygun |
| Mesaj, üye, tepki gibi olayları dinlemek | Uygun | Olmaz: HTTP'ye yalnızca etkileşimler gelir |
| Yalnızca komut, buton, menü ve form | Uygun | Uygun |
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:
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:
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.
| Kural | Neden |
|---|---|
Yalnızca https:// | İstek kişilerin verisini taşır; düz HTTP'de yolda okunabilir. |
Port yok ya da 443 | Baş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 olmaz | IP 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 karakter | Daha uzunu bir adres değil, veri taşıma girişimidir. |
| Ad, her bağlantıda genel bir IP'ye çözülmeli | 10.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 izlenmez | 301, 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
- Programını çalıştır ve adresin dışarıdan https ile açıldığını denetle.
- Geliştirici portalında uygulamanı aç, Etkileşimler bölümüne in.
- Herkese açık anahtar'ı Kopyala ile kopyala ve programına
NOLUYO_PUBLIC_KEYolarak ver. Anahtar her uygulamaya özeldir ve hiç değişmez. - Etkileşim adresi alanına adresini yaz, Kaydet ve test et'e bas.
- N'oluyo adresine iki
PINGyollar (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." - 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:
- 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.
- Zaman damgası şimdiden 5 dakikadan eskiyse ya da ilerideyse reddet. Böylece yakalanmış eski bir istek yeniden yollanamaz.
timestamp + gövdeüzerinde imzayı uygulamanın açık anahtarıyla doğrula.- Herhangi biri tutmazsa
401dö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.
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:
{ "id": "112794120000000000", "application_id": "112793931040686080", "type": 1, "token": "", "version": 1 }
| İstek | Beklenen yanıt |
|---|---|
Doğru imzalı PING | 200 ve gövdede {"type":1} |
Yanlış imzalı PING | 401 |
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:
| Durum | Gövde | Anlamı |
|---|---|---|
200 | JSON: { "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 204 | Boş | "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 şey | Baş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 istiyorsun | 200 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:
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:
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.
// 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
| Durum | Kişinin isteği | Kişi ne görür |
|---|---|---|
200 ve yanıt geçti | 200 | Botun yanıtı |
202, 204 | 200 | callback gelene dek bekler; 3 saniyede gelmezse "Bot yanıt vermedi" |
| Yanıt gelmedi, reddedildi ya da devre kesici açık | 504 application_no_response | "Bot zamanında yanıt vermedi." |
| Adresine aynı anda 16 teslim gidiyor ya da tüm botların tavanı dolu | 429 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
5dö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_busygö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ütun | Anlamı |
|---|---|
| Zaman | Teslimin anı |
| Tür | Etkileşimin türü: 2 komut, 3 bileşen, 4 otomatik tamamlama, 5 form |
| Durum | Adresinin HTTP durumu, hiç yanıt gelmediyse 0 |
| Süre | Milisaniye |
| Sonuç | Aşağıdaki tablo |
| Sonuç | Ne oldu | Ne yap |
|---|---|---|
ok | Başarılı | |
timeout | 3 saniyede yanıt gelmedi | İşi kısalt ya da önce 5 dön. |
dns | Ad çözülemedi | DNS kaydını denetle. |
connect | Bağlanılamadı | Sunucun ve 443 portu açık mı bak. |
tls | TLS kurulamadı | Sertifikan geçerli ve adla eşleşiyor mu bak. |
redirect | 3xx döndü | Son adresi kaydet. |
status | 200, 202 ve 204 dışında bir durum | Programının loguna bak; geçersiz imzaya 401 doğrudur, geçerliye değil. |
too_large | Yanıt 64 KiB'tan büyük | Yanıtı küçült. |
not_allowed | Ad yerel ya da özel bir IP'ye çözüldü | Adın genel bir IP göstermeli. |
bad_json | 200 geldi ama gövde tek bir JSON nesnesi değil | Gövdeyi JSON.stringify ile yaz, ardına bir şey ekleme. |
busy | Yanı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. |
error | Bizim tarafımızda bir hata | Sürerse bize bildir. |
| Bir hata kodu | Yanıt geldi ama reddedildi; callback rotası da aynı kodu dönerdi: interaction_response_invalid, validation_failed, dm_closed, application_unavailable gibi | Kodun 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
3000portunu 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ğruPING'e{"type":1}dönmeli.
Limitler
| Anahtar | Değer | Saniye | Anlamı |
|---|---|---|---|
verify | 5 | 60 | Kişi başına adres testi |
timeout | 3 | Bir teslimin tamamı | |
response | 65536 | Yanıtın en büyük boyutu, bayt | |
in_flight | 16 | Bir uygulamaya aynı anda giden teslim | |
trip | 10 | 30 | Bu kadar ardışık hatadan sonra adres bu kadar saniye aranmaz |
freshness | 300 | Kabul edeceğin en eski zaman damgası, saniye | |
url | 2048 | Adresin 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.
| Kod | Ne oldu | Ne yap |
|---|---|---|
endpoint_not_allowed | Adres kurallara uymuyor ya da ad yerel bir IP'ye çözülüyor | Yukarıdaki tabloya bak. |
endpoint_unreachable | Adrese ulaşılamadı ya da 3 saniyede yanıt gelmedi | Programın açık mı, https ve sertifika doğru mu bak. |
endpoint_bad_response | Doğru imzalı PING'e 200 {"type":1} dönmedi | type: 1 gelince {"type":1} dön; yönlendirme yapma. |
endpoint_no_signature_check | Yanlış imzalı isteğe 401 dönmedi | İmzayı her istekte doğrula, tutmazsa 401 dön. |
rate_limited | Dakikada 5 testi aştın | retry_after kadar bekle. |
Bu sayfayı yapay zekaya verirsen
Sırada ne var
- Etkileşimler: HTTP yanıt gövdesinde ya da callback rotasında dönen yanıt türleri
- JavaScript kütüphanesi ve Python kütüphanesi: yerleşik
verifyInteraction/verify_interactionyardımcıları - Güvenlik: imza doğrulama ve gizli anahtar yönetimi