Ana içeriğe geç

Örnekler

Biri JavaScript, biri Python ile yazılmış iki eksiksiz, en küçük bot. İkisi de aynı şeyi yapar:

  1. GET /gateway/bot ile gateway URL'sini alır,
  2. bağlanır, Hello'yu bekler, ardından Identify gönderir (yeniden bağlanmadan sonra Resume),
  3. Hello'dan gelen aralıkla heartbeat gönderir, heartbeat isteklerini yanıtlar ve ölü bağlantıyı algılar,
  4. READY içinden session_id değerini ve botun kullanıcı kimliğini hatırlar,
  5. her !ping mesajını bir pong yanıtıyla karşılar,
  6. bağlantı koptuğunda yeniden bağlanır ve resume eder; ölümcül bir kapanış kodunda durur.

Kullandıkları her şey Gateway bağlantısı ve Mesajlara yanıt verme sayfalarında anlatılır.

Başlamadan önce

  1. Bir bot oluşturun ve token'ını kopyalayın.
  2. Botu en az View Channel, Send Messages ve Read Message History izinleriyle (permissions=68608) bir sunucuya ekleyin.
  3. Token'ı kaynak dosyaya değil, her zaman bir ortam değişkenine koyun:
export TEBHOS_BOT_TOKEN="1290847135893127168.q3JxY2V0...W5k"
$env:TEBHOS_BOT_TOKEN = "1290847135893127168.q3JxY2V0...W5k"

Bot

Node.js 18 veya daha yenisi (yerleşik fetch için) ve ws paketi gerekir.

npm install ws

bot.mjs olarak kaydedin ve node bot.mjs ile çalıştırın.

// bot.mjs - minimal TEBHOS bot that answers "!ping" with "pong".
import WebSocket from "ws";

const TOKEN = process.env.TEBHOS_BOT_TOKEN; // "{application_id}.{secret}"
const API = process.env.TEBHOS_API ?? "https://tebhos.com/api/v1";

// Close codes after which reconnecting cannot help.
const FATAL_CLOSE_CODES = new Set([4004, 4010, 4011, 4012]);
// Close codes after which the old session cannot be resumed.
const FRESH_SESSION_CLOSE_CODES = new Set([4001, 4002, 4003, 4005, 4007]);

let gatewayUrl = null;
let sessionId = null;
let seq = null;
let botUserId = null;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// REST helper: bot auth header, JSON body, waits and retries on 429.
async function api(method, path, body) {
  for (;;) {
    const res = await fetch(`${API}${path}`, {
      method,
      headers: { Authorization: `Bot ${TOKEN}`, "Content-Type": "application/json" },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (res.status === 429) {
      const data = await res.json().catch(() => ({}));
      const seconds = Number(data.retry_after ?? res.headers.get("retry-after") ?? 1);
      await sleep(seconds * 1000);
      continue;
    }
    if (!res.ok) {
      throw new Error(`${method} ${path} failed: ${res.status} ${await res.text()}`);
    }
    return res.status === 204 ? null : res.json();
  }
}

async function handleMessage(message) {
  // Never answer yourself or other bots.
  if (message.author?.id === botUserId || message.author?.bot) return;
  if (message.content.trim() !== "!ping") return;
  await api("POST", `/channels/${message.channel_id}/messages`, {
    content: "pong",
    message_reference: { message_id: message.id },
  });
}

function handleDispatch(type, data) {
  switch (type) {
    case "READY":
      sessionId = data.session_id;
      botUserId = data.user.id;
      console.log(`Ready as ${data.user.username} (${botUserId})`);
      break;
    case "RESUMED":
      console.log("Session resumed");
      break;
    case "GUILD_CREATE":
      console.log(`Guild available: ${data.id}`);
      break;
    case "MESSAGE_CREATE":
      handleMessage(data).catch((error) => console.error(error));
      break;
  }
}

function connect() {
  const ws = new WebSocket(`${gatewayUrl}?v=1&encoding=json`);
  let heartbeatTimer = null;
  let firstBeatTimer = null;
  let acked = true;

  const send = (op, d) => ws.send(JSON.stringify({ op, d }));

  const beat = () => {
    if (!acked) {
      // No ACK since the last heartbeat: the connection is dead. Drop it and resume.
      ws.terminate();
      return;
    }
    acked = false;
    send(1, seq);
  };

  ws.on("message", (raw) => {
    const { op, d, s, t } = JSON.parse(raw.toString());
    if (s != null) seq = s;

    switch (op) {
      case 10: { // Hello
        const interval = d.heartbeat_interval;
        firstBeatTimer = setTimeout(() => {
          beat();
          heartbeatTimer = setInterval(beat, interval);
        }, interval * Math.random());

        if (sessionId && seq != null) {
          send(6, { token: TOKEN, session_id: sessionId, seq }); // Resume
        } else {
          send(2, { // Identify: the bare token, no "Bot " prefix
            token: TOKEN,
            properties: { os: process.platform, browser: "example-bot", device: "example-bot" },
          });
        }
        break;
      }
      case 11: // Heartbeat ACK
        acked = true;
        break;
      case 1: // The gateway asks for a heartbeat now
        send(1, seq);
        break;
      case 7: // Reconnect: drop this connection and resume on a new one
        ws.terminate();
        break;
      case 9: // Invalid Session: start over with a fresh Identify
        sessionId = null;
        seq = null;
        setTimeout(() => ws.terminate(), 1000 + Math.random() * 4000);
        break;
      case 0: // Dispatch
        handleDispatch(t, d);
        break;
    }
  });

  ws.on("close", (code) => {
    clearTimeout(firstBeatTimer);
    clearInterval(heartbeatTimer);
    if (FATAL_CLOSE_CODES.has(code)) {
      console.error(`Gateway closed with ${code}; not reconnecting.`);
      process.exit(1);
    }
    if (FRESH_SESSION_CLOSE_CODES.has(code)) {
      sessionId = null;
      seq = null;
    }
    console.log(`Disconnected (${code}); reconnecting in 2 s`);
    setTimeout(connect, 2000);
  });

  ws.on("error", (error) => console.error("Gateway error:", error.message));
}

if (!TOKEN) {
  console.error("Set TEBHOS_BOT_TOKEN first.");
  process.exit(1);
}
({ url: gatewayUrl } = await api("GET", "/gateway/bot"));
connect();

aiohttp ve websockets ile Python 3.10 veya daha yenisi gerekir.

pip install aiohttp websockets

bot.py olarak kaydedin ve python bot.py ile çalıştırın.

"""Minimal TEBHOS bot that answers "!ping" with "pong"."""
import asyncio
import json
import os
import random
import sys

import aiohttp
import websockets

TOKEN = os.environ.get("TEBHOS_BOT_TOKEN")  # "{application_id}.{secret}"
API = os.environ.get("TEBHOS_API", "https://tebhos.com/api/v1")

# Close codes after which reconnecting cannot help.
FATAL_CLOSE_CODES = {4004, 4010, 4011, 4012}
# Close codes after which the old session cannot be resumed.
FRESH_SESSION_CLOSE_CODES = {4001, 4002, 4003, 4005, 4007}


class Bot:
    def __init__(self, http: aiohttp.ClientSession):
        self.http = http
        self.session_id = None
        self.seq = None
        self.user_id = None
        self.acked = True
        self.tasks = set()

    async def api(self, method, path, body=None):
        """REST helper: bot auth header, JSON body, waits and retries on 429."""
        headers = {"Authorization": f"Bot {TOKEN}"}
        while True:
            async with self.http.request(method, API + path, json=body, headers=headers) as res:
                if res.status == 429:
                    try:
                        data = await res.json(content_type=None)
                        seconds = float(data.get("retry_after", 1))
                    except (ValueError, TypeError, AttributeError):
                        seconds = float(res.headers.get("Retry-After", 1))
                    await asyncio.sleep(seconds)
                    continue
                if res.status >= 400:
                    raise RuntimeError(f"{method} {path} failed: {res.status} {await res.text()}")
                return None if res.status == 204 else await res.json()

    async def send(self, ws, op, d):
        await ws.send(json.dumps({"op": op, "d": d}))

    async def heartbeat(self, ws, interval):
        await asyncio.sleep(interval * random.random())
        while True:
            if not self.acked:
                # No ACK since the last heartbeat: the connection is dead. Drop it and resume.
                await ws.close(code=4000)
                return
            self.acked = False
            await self.send(ws, 1, self.seq)
            await asyncio.sleep(interval)

    async def handle_message(self, message):
        author = message.get("author") or {}
        # Never answer yourself or other bots.
        if author.get("id") == self.user_id or author.get("bot"):
            return
        if message.get("content", "").strip() != "!ping":
            return
        await self.api("POST", f"/channels/{message['channel_id']}/messages", {
            "content": "pong",
            "message_reference": {"message_id": message["id"]},
        })

    def spawn(self, coro):
        task = asyncio.create_task(coro)
        self.tasks.add(task)
        task.add_done_callback(self.tasks.discard)

    def handle_dispatch(self, event, data):
        if event == "READY":
            self.session_id = data["session_id"]
            self.user_id = data["user"]["id"]
            print(f"Ready as {data['user']['username']} ({self.user_id})")
        elif event == "RESUMED":
            print("Session resumed")
        elif event == "GUILD_CREATE":
            print(f"Guild available: {data['id']}")
        elif event == "MESSAGE_CREATE":
            self.spawn(self.handle_message(data))

    async def connect_once(self, url):
        """Runs one gateway connection. Returns the close code, if any."""
        heartbeat_task = None
        try:
            async with websockets.connect(url, max_size=None) as ws:
                async for raw in ws:
                    payload = json.loads(raw)
                    op, d = payload["op"], payload.get("d")
                    if payload.get("s") is not None:
                        self.seq = payload["s"]

                    if op == 10:  # Hello
                        self.acked = True
                        heartbeat_task = asyncio.create_task(
                            self.heartbeat(ws, d["heartbeat_interval"] / 1000))
                        if self.session_id and self.seq is not None:
                            await self.send(ws, 6, {  # Resume
                                "token": TOKEN, "session_id": self.session_id, "seq": self.seq})
                        else:
                            await self.send(ws, 2, {  # Identify: the bare token, no "Bot " prefix
                                "token": TOKEN,
                                "properties": {"os": sys.platform, "browser": "example-bot",
                                               "device": "example-bot"},
                            })
                    elif op == 11:  # Heartbeat ACK
                        self.acked = True
                    elif op == 1:  # The gateway asks for a heartbeat now
                        await self.send(ws, 1, self.seq)
                    elif op == 7:  # Reconnect: drop this connection and resume on a new one
                        await ws.close(code=4000)
                    elif op == 9:  # Invalid Session: start over with a fresh Identify
                        self.session_id, self.seq = None, None
                        await asyncio.sleep(random.uniform(1, 5))
                        await ws.close(code=4000)
                    elif op == 0:  # Dispatch
                        self.handle_dispatch(payload.get("t"), d)
                return ws.close_code
        except websockets.ConnectionClosed as closed:
            return closed.rcvd.code if closed.rcvd else None
        except OSError as error:
            print(f"Connection failed: {error}")
            return None
        finally:
            if heartbeat_task:
                heartbeat_task.cancel()

    async def run(self):
        gateway = await self.api("GET", "/gateway/bot")
        url = gateway["url"] + "?v=1&encoding=json"
        while True:
            code = await self.connect_once(url)
            if code in FATAL_CLOSE_CODES:
                sys.exit(f"Gateway closed with {code}; not reconnecting.")
            if code in FRESH_SESSION_CLOSE_CODES:
                self.session_id, self.seq = None, None
            print(f"Disconnected ({code}); reconnecting in 2 s")
            await asyncio.sleep(2)


async def main():
    if not TOKEN:
        sys.exit("Set TEBHOS_BOT_TOKEN first.")
    async with aiohttp.ClientSession() as http:
        await Bot(http).run()


if __name__ == "__main__":
    asyncio.run(main())

Botu başlatın, görebildiği bir kanala !ping yazın; bot pong ile yanıt verir.

Parçaların protokolle eşleşmesi

Kod Protokol adımı Başvuru
GET /gateway/bot WebSocket URL'sini bul Gateway URL'sini al
?v=1&encoding=json Zorunlu gateway sürümü ve JSON çerçeveleri Bağlan
op 10 işleyicisi Heartbeat göndermeye başla, ardından Identify veya Resume Hello al
properties ile op 2 Çıplak token ile kimlik doğrula Identify
acked bayrağı Yanıt vermeyi bırakan bağlantıyı algıla Heartbeat
READY işleyicisi session_id değerini ve botun kullanıcı kimliğini sakla READY al
Kopmadan sonra op 6 Resume et ve kaçırılan olayları al Resume
MESSAGE_CREATE işleyicisi Önekli komut ve yanıt Mesajlara yanıt verme
api içindeki 429 döngüsü retry_after saniye bekle, ardından yeniden dene Hız sınırları

Daha fazla komut ekleme

Tek !ping denetimini bir arama tablosuyla değiştirin:

const commands = {
  ping: async (message) => ({ content: "pong" }),
  echo: async (message, args) => ({ content: args.join(" ") || "Nothing to echo." }),
};

async function handleMessage(message) {
  if (message.author?.id === botUserId || message.author?.bot) return;
  const content = message.content.trim();
  if (!content.startsWith("!")) return;
  const [name, ...args] = content.slice(1).split(/\s+/);
  const command = commands[name.toLowerCase()];
  if (!command) return;
  const reply = await command(message, args);
  await api("POST", `/channels/${message.channel_id}/messages`, {
    ...reply,
    message_reference: { message_id: message.id },
  });
}
async def cmd_ping(message, args):
    return {"content": "pong"}

async def cmd_echo(message, args):
    return {"content": " ".join(args) or "Nothing to echo."}

COMMANDS = {"ping": cmd_ping, "echo": cmd_echo}

# inside class Bot:
async def handle_message(self, message):
    author = message.get("author") or {}
    if author.get("id") == self.user_id or author.get("bot"):
        return
    content = message.get("content", "").strip()
    if not content.startswith("!"):
        return
    name, *args = content[1:].split() or [""]
    command = COMMANDS.get(name.lower())
    if command is None:
        return
    reply = await command(message, args)
    reply["message_reference"] = {"message_id": message["id"]}
    await self.api("POST", f"/channels/{message['channel_id']}/messages", reply)

Gerçekten çalıştırmadan önce

  • Token'ı kaynak denetiminden ve günlüklerden uzak tutun. Sızarsa yeniden oluşturun; çalışan botların bağlantısı kesilir ve yeni token ile yeniden başlatılmaları gerekir.
  • Kodunuza yalnızca ihtiyaç duyduğunuz olayları iletin: geri kalanını gateway'de düşürmek için Identify içinde ignored_events verin; bkz. Olay filtreleme.
  • Gönderimlerde 403'ü düzgün işleyin: bir sunucu botun izinlerini herhangi bir kanalda, herhangi bir anda kaldırabilir.
  • Kanal başına mesaj sınırının (10 saniyede 20) altında kalın; yanıtları toplu göndermek yerine kuyruğa alın.