N'oluyo · Geliştirici Dokümanları · Son güncelleme: · https://noluyo.net/gelistirici

Ö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.

Ö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.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.

// 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);
# 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
    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 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.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.

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.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.

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.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 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

Sırada ne var