Örnekler¶
Biri JavaScript, biri Python ile yazılmış iki eksiksiz, en küçük bot. İkisi de aynı şeyi yapar:
GET /gateway/botile gateway URL'sini alır,- bağlanır, Hello'yu bekler, ardından Identify gönderir (yeniden bağlanmadan sonra Resume),
- Hello'dan gelen aralıkla heartbeat gönderir, heartbeat isteklerini yanıtlar ve ölü bağlantıyı algılar,
READYiçindensession_iddeğerini ve botun kullanıcı kimliğini hatırlar,- her
!pingmesajını birpongyanıtıyla karşılar, - 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¶
- Bir bot oluşturun ve token'ını kopyalayın.
- Botu en az View Channel, Send Messages ve Read Message History izinleriyle (
permissions=68608) bir sunucuya ekleyin. - Token'ı kaynak dosyaya değil, her zaman bir ortam değişkenine koyun:
Bot¶
Node.js 18 veya daha yenisi (yerleşik fetch için) ve ws paketi gerekir.
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.
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_eventsverin; 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.