Anten
English

Geliştiriciler

Bot yazmak

Bot, Anten'e üye gibi bağlanan bir program: komutlara ve butonlara cevap verir, şifreli ve sesli odalarda da çalışır. İlk bot yaklaşık 10 dakika sürer.

Kimin için
JavaScript yazabilen geliştiriciler
Süre
İlk bot için yaklaşık 10 dakika
Gerekenler
Node.js 22.12+, Botları yönetme izni
Sonuç
/ping komutuna cevap veren bir bot

Bir bot neler yapar

Bot, sizin yazdığınız bir program. Anten’e üye gibi bağlanır, mesaj okuyup yazar ve olayları (yeni mesaj, tepki, odaya giren biri) olduğu anda duyar. Bot hesabı programın kimliğidir; program bu hesabın anahtarıyla bağlanır.

Bir bot şunları yapabilir:

  • Komut sunmak: kişiler yazma kutusuna / yazınca botun komutları önerilir.
  • Mesajlarına kartlar, butonlar ve seçim menüleri eklemek; basılınca yalnızca basan kişiye cevap vermek.
  • Şifreli odalarda, kişilerin uygulamaları gibi kendi anahtarlarıyla okuyup yazmak.
  • Sesli odaya girip ses çalmak.

Tek ihtiyacınız başka bir sistemden bir odaya mesaj yazmaksa webhook daha basit: sürekli bağlantı da kütüphane de gerekmez.

Örnekler anten-bot kütüphanesini kullanır. Kütüphane Anten’in dosyalarıyla gelir (sdk/js klasörü), Node.js 22.12 ve üstünde çalışır ve başka paket gerektirmez. Bağlantıyı açık tutar, koparsa yeniden bağlanır ve şifreli odaları kendisi yönetir.

İlk botunuz

  1. Botu oluşturun.

    Sunucu ayarları → Botlar bölümünde bir ad yazın ve Bot oluştur’a basın. Bunun için ilk sunucuda Botları yönetme izni gerekir; yöneticilerde vardır.

  2. Anahtarı kopyalayın.

    Anahtar anten_bot_ ile başlar ve yalnızca bir kez gösterilir. Hemen güvenli bir yere kaydedin.

  3. Kütüphaneyi botun klasörüne kopyalayın.

    Anten’in dosyaları bir üst klasördeki anten klasöründeyse:

    mkdir benim-botum && cd benim-botum
    cp -r ../anten/sdk/js anten-bot
    
  4. bot.mjs dosyasını yazın.

    import { Client } from './anten-bot/src/index.js';
    
    const bot = new Client({
    	host: process.env.ANTEN_HOST, // e.g. anten.example.org
    	token: process.env.ANTEN_BOT_TOKEN,
    	keyDir: '.anten-bot', // the bot's own keys
    });
    
    bot.on('ready', () => bot.setCommands([{ name: 'ping', description: 'Botun cevap verdiğini sınar' }]));
    
    bot.on('command', async ({ name, reply }) => {
    	if (name === 'ping') await reply('pong');
    });
    
    bot.on('error', (err) => console.error(err));
    bot.on('close', (code, reason) => console.error('stopped:', code, reason));
    
    await bot.start();
    
  5. Botu çalıştırın.

    export ANTEN_HOST=anten.example.org
    export ANTEN_BOT_TOKEN=anten_bot_…
    node bot.mjs
    
  6. Deneyin.

    Botun görebildiği bir odada /ping yazın. Bot pong diye cevap verir.

Bot her sunucuya üye olur ve varsayılan gruplara girer. Hangi odaları göreceğini ve nereye yazabileceğini, bir kişide olduğu gibi gruplarıyla ve oda izinleriyle belirlersiniz. Botların adının yanında BOT etiketi görünür. Botun resmini aynı bölümde Resmi değiştir ile koyabilirsiniz.

Komutlar

Bot, sunduğu komutları setCommands ile bildirir. Kişiler / yazınca, o odayı görebilen ve oraya yazabilen botların komutları açıklamalarıyla önerilir. Kullanılan komut sıradan bir mesajdır: şifreli odada şifreli gider ve sunucu içeriğini görmez. Kütüphane mesajı okur, seçenekleri ayırır ve command olayını verir.

bot.on('ready', () =>
	bot.setCommands([
		{
			name: 'zar',
			description: 'Zar atar',
			options: [{ name: 'yüz', description: 'Kaç yüzlü', type: 'integer' }],
		},
		{
			name: 'selam',
			description: 'Birini selamlar',
			options: [{ name: 'kim', description: 'Selamlanacak kişi', type: 'user', required: true }],
		},
	]),
);

bot.on('command', async ({ name, args, message, reply }) => {
	if (name === 'zar') await reply(`🎲 ${1 + Math.floor(Math.random() * (args.yüz ?? 6))}`);
	if (name === 'selam') {
		const member = bot.guilds.get(message.guild_id)?.members.get(args.kim);
		await reply(`Merhaba ${member?.user.display_name}!`);
	}
});

Seçenekler

  • Türler: string (metin), integer (tam sayı), number (sayı), boolean (evet/hayır, açık/kapalı, true/false) ve user (bir üyenin adı ya da @adı; bota kullanıcının kimliği gelir).
  • Komutun son seçeneği metinse (string), satırın geri kalanını alır. Boşluk içeren bir metin tırnak içinde de yazılabilir.
  • Zorunlu seçenekler (required: true) önce gelir.
  • Eksik ya da hatalı kullanımda kütüphane kullanım satırıyla cevap verir, örneğin ⚠ /selam <kim>. Cevabı kendiniz vermek için commandError olayını dinleyin.
  • İki bot aynı komut adını kullanırsa öneri /zar@Asistan biçiminde yazılır (botun adı, boşluksuz).

Hazır değerler ve tamamlama

Bir seçeneğe hazır değerler (choices) verebilirsiniz; kişi yazarken önerilir:

{ name: 'öncelik', description: 'Ne kadar acil', type: 'string', choices: [{ name: 'Yüksek', value: 'yuksek' }, { name: 'Düşük', value: 'dusuk' }] }

Değerler önceden bilinmiyorsa seçeneğe autocomplete: true ekleyin. Kişi yazdıkça bota sorulur; bot en fazla 25 öneriyle cevap verir:

bot.on('autocomplete', async ({ command, option, query, choices }) => {
	const cities = ['İstanbul', 'İzmir', 'Ankara'];
	await choices(cities.filter((c) => c.toLocaleLowerCase('tr').startsWith(query.toLocaleLowerCase('tr'))));
});

Butonlar ve menüler

Bir mesaja kartlar (embeds) ve butonlarla seçim menüleri (components) eklenebilir. Bunlar mesajın içinde taşınır, yani şifreli odada mesajla birlikte şifrelenir. Uygulama bunları yalnızca botların mesajlarında çizer ve dışarıdan resim yüklemez.

await bot.send(roomId, 'Bugün ne yiyelim?', {
	embeds: [{ title: 'Öğle yemeği', description: 'Birini seçin', color: 0x4fd1a5, fields: [{ name: 'Süre', value: '5 dk', inline: true }] }],
	components: [
		[
			{ type: 'button', id: 'evet', label: 'Katılıyorum', style: 'success' },
			{ type: 'button', id: 'hayir', label: 'Bugün yokum', style: 'danger' },
		],
		[{ type: 'select', id: 'yemek', placeholder: 'Seçin', options: [{ label: 'Çorba', value: 'corba' }, { label: 'Makarna', value: 'makarna' }] }],
		[{ type: 'link', label: 'Menü', url: 'https://anten.example.org/menu' }],
	],
});
  • Buton stilleri: primary, secondary, success, danger. Bağlantı butonu (link) yalnızca http(s) adreslerine gider.
  • Bir satırda en fazla 5 buton ya da tek bir menü olur; bir mesajda en fazla 5 satır ve 5 kart bulunur.
  • Bir menüde en fazla 25 seçenek olur. min ve max ile birden fazla seçim istenebilir.
  • Mesajı sonradan bot.edit(message, text, { embeds, components }) ile değiştirebilirsiniz.

Etkileşimler ve cevaplar

Bir basış ya da menü seçimi bota etkileşim olarak gelir. Bot buna reply ile cevap verir. Cevabı yalnızca basan kişi görür ve sayfa yenilenince kaybolur.

bot.on('interaction', async ({ customId, values, user_id, reply }) => {
	if (customId === 'yemek') await reply(`Seçiminiz: ${values[0]}`);
	else await reply(customId === 'evet' ? 'Sizi listeye ekledim.' : 'Peki, bir dahaki sefere.');
});

Şifreli odalar

Uçtan uca şifreli bir odada sunucu mesajları okuyamaz. Bot bu odalarda, kişilerin uygulamaları gibi kendi anahtarlarıyla çalışır. Bunu anten-bot kendisi halleder; aynı kod iki tür odada da çalışır.

  • Bot ilk açılışta bir kimlik anahtarı ve bir cihaz anahtarı üretir. İkisini keyDir klasöründe, yalnızca kendi kullanıcısının okuyabileceği bir dosyada saklar.
  • Odadakilerin uygulamaları botun anahtarını ilk görüşte tanır ve odanın anahtarını ona da verir. Gerektiğinde bot yeni bir oda anahtarını kendisi oluşturur.
  • Bot mesajlarını kişilerinkiyle aynı biçimde şifreler ve imzalar.
  • Yazarının bilinen anahtarıyla imzalanmamış bir mesaj trusted: false ile gelir ve identityChanged olayı yayılır. Yeni anahtarı kişiyle Anten dışında doğruladıktan sonra bot.acceptIdentity(userId, key) ile kabul edin.

keyDir bir parola kadar değerlidir: onu ele geçiren, botun okuyabildiği odaları okuyabilir. Yedekleyin, ama paylaşmayın ve bir depoya koymayın. Kaybederseniz bot yeni bir kimlikle başlar ve odadakilere botun anahtarının değiştiği bildirilir.

Anahtar ya da keyDir sızarsa Anahtarı yenile’ye basın: botun cihazları silinir, tuttuğu oda anahtarları geri çekilir ve her oda bir sonraki mesajla yeni bir anahtara geçer. Sızan anahtarlarla eski mesajlar okunabilir, yenileri okunamaz.

Sesli botlar

Bir bot sesli odaya girip ses çalabilir; sesi kişilerinki gibi uçtan uca şifrelenir. Sesi, Anten’in dosyalarındaki ses yardımcısı gönderir: bots/voice klasöründe Go ile yazılmış anten-voice programı. Dosyaları ve adresleri ffmpeg ile çalar. Hazır müzik botu da bu yolla çalışır. Programı botun klasöründe derleyin:

go build -C ../anten/bots/voice -o "$PWD/anten-voice" .

Kütüphanenin VoiceHelper sınıfı bu programı başlatır. Bot bağlandıktan sonra (ready) odaya girebilir:

import { VoiceHelper } from './anten-bot/src/index.js';

const helper = new VoiceHelper({ path: './anten-voice', ffmpeg: 'ffmpeg' });

await bot.joinVoice(guildId, roomId, helper); // deaf by default
helper.play('muzik.ogg', { volume: 0.8 });
helper.on('ended', (error) => console.log('ended', error));

Bot odaya varsayılan olarak sağır girer. Bu yalnızca bir işaret değil:

  • Sunucu sağır bir bota kimsenin sesini göndermez.
  • Odadakilerin uygulamaları ona ses anahtarı vermez; bot sağır olunca odadakiler botun almadığı yeni bir anahtara geçer.
  • Bot sağırlığını kaldırırsa ({ deaf: false }) bu herkese görünür; anahtarları voiceKey olayıyla alır. Hazır ses yardımcısı yalnızca ses gönderir; gelen sesi çözmek sizin kodunuza kalır.
  • Mikrofonu kapalı (bot.setVoice({ mute: true })) bir botun sesini sunucu iletmez.

Başka dillerden bağlanmak

Kütüphane olmadan da bağlanabilirsiniz; şifresiz odalar için bu yeter. Şifreli bir odada sunucu düz bir mesajı sealed_required hatasıyla geri çevirir.

REST: Adresler https://anten.example.org/api/v1/... biçimindedir. Her istek Authorization: Bot <anahtar> başlığını taşır; gövdeler JSON’dur. Kimlikler JSON’da metin olarak gelir. Hatalar {"code": "...", "message": "..."} biçimindedir.

Gateway: Canlı olaylar wss://anten.example.org/gateway adresinden, aynı başlıkla gelir. Her çerçeve {"op", "d", "s", "t"} biçimindedir.

  1. Sunucunun ilk çerçevesini (op 10) okuyun.

    d.heartbeat_interval kalp atışı aralığıdır (milisaniye).

  2. Kendinizi tanıtın.

    {"op": 2, "d": {}} gönderin. Sunucu READY olayıyla sunucuları, odaları ve üyeleri verir.

  3. Kalp atışını sürdürün.

    Her aralıkta {"op": 1, "d": <son s>} gönderin; sunucu op 11 ile cevaplar. Kalp atışı kesilen bağlantı 4009 koduyla kapanır.

  4. Olayları işleyin.

    Olaylar op 0 ile gelir, adı t alanındadır: MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_REACTION_ADD, GUILD_MEMBER_ADD, VOICE_STATE_UPDATE…

Bağlantı koparsa biraz bekleyip yeniden bağlanın. Yalnızca 4004 (anahtar yenilendi ya da bot silindi), 4006 (sunucudan atıldı) ve 4007 (yasaklandı) kodlarında yeniden bağlanmayın. Botun kendi mesajları da MESSAGE_CREATE olarak gelir: yazar botun kendisiyse cevap vermeyin, yoksa bot kendi kendisiyle konuşur.

Python ile !ping yazana cevap veren bir örnek (Python 3.10 ve üstü, pip install websockets):

import asyncio, json, os, urllib.request
import websockets

HOST = os.environ["ANTEN_HOST"]
AUTH = {"Authorization": "Bot " + os.environ["ANTEN_BOT_TOKEN"]}

def api(method, path, body=None):
    req = urllib.request.Request(
        f"https://{HOST}/api/v1{path}", method=method,
        data=json.dumps(body).encode() if body is not None else None,
        headers={**AUTH, **({"Content-Type": "application/json"} if body is not None else {})},
    )
    with urllib.request.urlopen(req) as res:
        return json.loads(res.read() or "null")

ME = api("GET", "/users/@me")

async def run():
    while True:
        try:
            async with websockets.connect(f"wss://{HOST}/gateway", additional_headers=AUTH) as ws:
                seq = 0
                hello = json.loads(await ws.recv())
                await ws.send(json.dumps({"op": 2, "d": {}}))

                async def beat():
                    while True:
                        await asyncio.sleep(hello["d"]["heartbeat_interval"] / 1000)
                        await ws.send(json.dumps({"op": 1, "d": seq}))

                beater = asyncio.create_task(beat())
                try:
                    async for raw in ws:
                        f = json.loads(raw)
                        seq = f.get("s") or seq
                        if f["op"] == 0 and f["t"] == "MESSAGE_CREATE":
                            m = f["d"]
                            if (m.get("author") or {}).get("id") == ME["id"] or m.get("dm"):
                                continue
                            if m["content"].strip() == "!ping":
                                await asyncio.to_thread(api, "POST", f"/rooms/{m['room_id']}/messages",
                                                        {"content": "pong", "reply_to": m["id"]})
                finally:
                    beater.cancel()
        except websockets.ConnectionClosed as e:
            if e.rcvd and e.rcvd.code in (4004, 4006, 4007):
                raise SystemExit(f"closed: {e.rcvd.code} {e.rcvd.reason}")
        await asyncio.sleep(5)

asyncio.run(run())

Sınırlar ve güvenlik

Botlar şunlara hiçbir zaman ulaşamaz, yönetici grubunda olsalar bile:

  • Özel mesajlar: bot okuyamaz ve yazamaz; kimse bota özel mesaj gönderemez.
  • Kurulumun yönetimi: erişim, yedekler, güncellemeler, sunucu durumu ve botlar.
  • Yetki anahtarları, hesaplar ve anahtar yedekleri.

Böylece sızan bir anahtar kurulumu ele geçiremez. Diğer sınırlar:

  • Bir kurulumda en fazla 50 bot olur.
  • Bir botun en fazla 25 komutu, her komutun en fazla 10 seçeneği, her seçeneğin en fazla 25 hazır değeri olur. Komut adları küçük harf (Türkçe harfler dahil), rakam, _ ve - içerir.
  • Yazma istekleri (mesaj, tepki, yazıyor bilgisi) art arda 10, sonra saniyede 2 olabilir. Fazlası 429 alır: Retry-After başlığındaki saniye kadar bekleyin. Kütüphane bunu kendisi yapar.

İyi alışkanlıklar:

  • Anahtarı yalnızca botun çalıştığı makinede, bir ortam değişkeninde ya da bir gizli bilgi kasasında tutun.
  • Botu yönetici yapmayın; yalnızca ihtiyaç duyduğu odaları gören bir gruba koyun.
  • Komutu kimin kullanabileceğine bot karar verir: message.author ve bot.guilds.get(message.guild_id).members ile kişinin gruplarına bakın.
  • Kişilerden gelen metni bir komut satırına ya da adrese olduğu gibi koymayın.

Başvuru

Client

new Client({ host, token, keyDir }) bir bot oluşturur; await bot.start() bağlanır, await bot.stop() kapatır.

Yöntem Ne yapar
send(roomId, text, { replyTo, embeds, components }) Mesaj gönderir; oda şifreliyse şifreler
reply(message, text, extra) Bir mesaja cevap verir
edit(message, text, { embeds, components }) Botun kendi mesajını değiştirir
delete(message) Mesajı siler
react(message, emoji) Tepki ekler
typing(roomId) Birkaç saniye “yazıyor” gösterir
messages(roomId, { before, limit }) Odanın mesajları, eskiden yeniye, açılmış olarak
setCommands(commands) Botun komutlarını bildirir
setActivity({ kind, name }) Adının yanında ne yaptığını gösterir; null kaldırır
joinVoice(guildId, roomId, helper, { mute, deaf }) Sesli odaya girer
setVoice({ mute, deaf }) Odadayken mikrofonu ya da sağırlığı değiştirir
leaveVoice() Sesli odadan çıkar
acceptIdentity(userId, key) Doğrulanmış yeni bir kimlik anahtarını kabul eder
api(method, path, body) Bir REST isteği gönderir

bot.me botun kendisi, bot.guilds sunucuları (her birinde guild, rooms, members), bot.voiceFlags ses durumudur. setActivity için kind değeri playing, listening ya da watching olur; ad en fazla 64 karakterdir.

Olaylar

Olay Ne zaman
ready Bağlantı kuruldu
message, messageUpdate, messageDelete Bir mesaj geldi, değişti ya da silindi (text açılmış metindir)
reaction Bir mesaja tepki eklendi
command, commandError Botun bir komutu kullanıldı ya da yanlış kullanıldı
interaction, autocomplete Bir butona basıldı, bir menüden seçildi ya da bir seçenek yazılıyor
identityChanged Bir üyenin kimlik anahtarı bilinenden farklı
voice, voiceKey Botun sesli odası değişti; sağır değilken bir ses anahtarı geldi
event Gateway’den gelen her olay (adı ve verisi)
error, close Bir hata oldu; bağlantı kalıcı olarak kapandı

REST

İstek Ne yapar
GET /users/@me Botun kendisi
GET /users/@me/guilds Botun sunucuları
GET /guilds/{sunucu} Sunucunun odaları, grupları ve üyeleri
GET /rooms/{oda}/messages?before={id}&limit=50 Odanın mesajları
POST /rooms/{oda}/messages Mesaj gönderir: {"content": "...", "reply_to": "{id}"}
PATCH /rooms/{oda}/messages/{id} Botun kendi mesajını düzenler
DELETE /rooms/{oda}/messages/{id} Mesajı siler
POST /rooms/{oda}/messages/{id}/reactions Tepki ekler: {"emoji": "👍"}
POST /rooms/{oda}/typing “Yazıyor” gösterir
PUT /users/@me/commands Komutları bildirir
PUT /users/@me/avatar Botun resmini değiştirir (gövde: resim)