Ö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 sayfasındadır. curl örnekleri için ayrıca jq gerekir.
Başlamadan önce botunu oluşturup sunucuna ekle (Hızlı başlangıç 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:
export BOT_TOKEN='kopyaladigin_token'
export CHANNEL_ID='kanal id degeri'Ortak yardımcılar
Olayları dinlemek için Gateway 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.
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.
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:
node ping.mjsmessage_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.
# 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.
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.
# 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
doneBilmen gerekenler:
Mesaj silmek için rol sırası önemli değildir;
ManageMessagesyeterlidir. Kanal ayarları bu izni kaldırabilir, o zaman403 missing_permissionsalırsın.Silme rotaları
writesınıfındadır: kanal başına 10 saniyede 10 istek.api.mjssı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 rotası var. 2 ile 100 arası mesaj alır, hiçbiri 14 günden eski olamaz ve saniyede bir istek kabul eder:
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 ile Etkileşimler 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.
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:
node anket.mjsBilmen 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,
SendMessagesizni 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.
export GUILD_ID='sunucu id degeri'
export ROLE_IDS='rol id,diger rol id'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);
},
});node rol-secici.mjsBilmen gerekenler:
Rol istekleri ilk yanıttan sonra gider. Bot önce
type: 7ile 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: 7her 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.
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);
},
});node sayfali-liste.mjsBilmen gerekenler:
Gidilecek sayfa
custom_id'nin içinde durur, bot ek bir kayıt tutmaz. Sunucucustom_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 aynı yolu kullanır.
Butonları kapatan
PATCHyalnızcacomponentsgö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
Sırada ne var
Botu sürekli çalışır tutarken token'ı nasıl koruyacağın: Güvenlik
Bağlantının ayrıntıları: Gateway
Buton, menü ve embed'in ayrıntıları: Bileşenler, Etkileşimler, Embed'ler
Bir şey ters gidiyorsa: Sık sorulan sorular
Başka neler yapılabileceği: REST referansı