API Echo
Publiczne REST API i webhooki: podłącz ekonomię, poziomy i monitoring serwera do własnych usług — serwera gry, strony, bota albo automatyzacji.
https://api.echo-assistants.tech
Autoryzacja
Każde żądanie wymaga klucza w nagłówku Authorization. Klucz jest wydawany na serwer, a nie na konto. Utworzyć go może tylko właściciel serwera w panelu → „Dla deweloperów”: do 3 (z Premium — 10) kluczy. Endpointy /catalog/* są publiczne — klucz nie jest potrzebny (limit 60/min na IP, odpowiedzi są buforowane).
curl https://api.echo-assistants.tech/me \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Bezpieczeństwo kluczy
Klucz zaczyna się od echoapi_, potem ma 30 losowych łacińskich liter i cyfr oraz 6-znakową sumę kontrolną. Ciąg z błędną sumą kontrolną API odrzuca od razu (401). Klucze w starym formacie (echo_…) nadal działają; klucz wygenerowany ponownie dostaje nowy format.
\bechoapi_[0-9A-Za-z]{36}\bKlucz, który trafił do publicznego dostępu, jest wyłączany automatycznie. W ramach programu Secret Scanning GitHub zgłasza nam klucze znalezione w publicznych repozytoriach, a bot Echo wykrywa klucze w wiadomościach na serwerach i usuwa takie wiadomości. Właściciel serwera i twórca klucza dostają powiadomienie na stronie, na Telegramie i e-mailem — z linkiem do miejsca znaleziska.
Co zrobić: otwórz klucze serwera w portalu dewelopera i kliknij Wygeneruj ponownie na karcie klucza. Klucz dostanie nowy sekret i znowu zacznie działać, a stary przestanie działać od razu. Zamień klucz wszędzie, gdzie jest zapisany. Znalezionego klucza nie da się włączyć bez ponownego wygenerowania. Jeśli klucz nie jest już potrzebny, unieważnij go.
Przechowuj klucz na własnym serwerze — w zmiennych środowiskowych albo w magazynie sekretów. Nie umieszczaj go w kodzie stron i aplikacji, w repozytoriach, wiadomościach ani na zrzutach ekranu.
Usługa
/mebez zakresuSprawdzenie klucza: zwraca serwer i jego zakresy.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Ekonomia
/economy/balance?user=<id>economy:readSaldo członka: portfel, bank, żetony.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writeDodaj lub odejmij walutę. delta może być ujemna; saldo pozostaje w zakresie 0…2 000 000 000.
curl -X POST "https://api.echo-assistants.tech/economy/balance" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "user": "123...", "delta": 250 }'{ "user": "123...", "delta": 250 }{ "ok": true, "user": "123...", "balance": 1750 }/economy/leaderboard?limit=<n>economy:readTop członków według salda (limit 1–100, domyślnie 20).
curl "https://api.echo-assistants.tech/economy/leaderboard?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "balance": 15000, "bank": 3000 } ] }Poziomy
/levels/leaderboard?limit=<n>levels:readTop członków według doświadczenia (limit 1–100, domyślnie 20).
curl "https://api.echo-assistants.tech/levels/leaderboard?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "xp": 3965, "messages": 374, "voiceSeconds": 1273 } ] }/levels/xp?user=<id>levels:readXP i poziom członka; nextLevelXp to łączna liczba XP potrzebna do następnego poziomu.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeDodanie lub odjęcie XP: delta to liczba całkowita, do ±1 000 000 na zapytanie; XP nie spada poniżej zera. Poziom jest od razu przeliczany. Jeśli wzrósł, bot przyznaje nagrody za poziom (role, monety, ogłoszenie) jak przy zwykłym zdobywaniu poziomów i wysyła zdarzenie member.level_up z oznaczeniem "via": "api". Obniżenie poziomu nie odbiera nagród. Tylko dla członków serwera (w przeciwnym razie 404 not_member); jeśli poziomy są wyłączone na serwerze — 409 levels_off.
curl -X POST "https://api.echo-assistants.tech/levels/xp" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "user": "123...", "delta": 250 }'{ "user": "123...", "delta": 250 }{ "ok": true, "user": "123...", "xp": 1570, "level": 5, "previousXp": 1320, "previousLevel": 4, "levelUp": true }Monitoring
/monitoring/servermonitoring:readKarta serwera w katalogu: Płomienie za okres, łącznie, miejsce w topie i statusy.
curl "https://api.echo-assistants.tech/monitoring/server" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "slug": "my-server", "flames": 420, "flamesTotal": 9100, "published": true, "rank": 3, "partner": true, "verified": true }/monitoring/top?limit=<n>monitoring:readTop serwerów katalogu według Płomieni (limit 1–100, domyślnie 20).
curl "https://api.echo-assistants.tech/monitoring/top?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "top": [ { "rank": 1, "guildId": "1369...", "slug": "my-server", "flames": 980, "flamesTotal": 21000 } ] }Integracje
Podłącz własną grę, stronę lub CRM: atrybuty członków zamieniają się w role według reguł z panelu → „Integracje”, gracze wiążą konta kodem, a dozwolone role można nadawać bezpośrednio. Treść żądań zapisu — JSON do 8 KB.
/members/<discordId>/attributesmembers:writePrzekaż atrybuty członka (pełna zamiana): do 25 pól, nazwa — litery łacińskie/cyfry/_, wartość — tekst do 100 znaków, liczba lub true/false. Bot od razu przeliczy role według reguł.
curl -X PUT "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "attributes": { "faction": "police", "rank": 3, "vip": true } }'{ "attributes": { "faction": "police", "rank": 3, "vip": true } }{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }/members/<discordId>/attributesmembers:readCo przesłano dla członka i jakie konto zewnętrzne jest z nim powiązane.
timedRoles — role dozwolone dla API, nadane na określony czas, i moment, w którym bot je zdejmie.
curl "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "discordId": "123...", "externalId": "Ivan_Petrov", "attributes": { "faction": "police", "rank": 3 }, "timedRoles": [ { "roleId": "1369...", "expiresAt": "2026-10-10T12:00:00.000Z" } ] }/members/<discordId>/attributesmembers:writeUsuń wszystkie atrybuty członka. Role nadane przez regułę bot odbierze, a nadanych ręcznie nie rusza.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeTo samo według ID w twoim systemie (nick, UUID, license:…). Działa na osobę, która powiązała konto kodem; dopóki konto nie jest powiązane, atrybuty są przechowywane i zostaną zastosowane przy powiązaniu. Przy zgodnej nazwie pierwszeństwo mają atrybuty według Discord ID.
curl -X PUT "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "attributes": { "faction": "army", "rank": 5 } }'{ "attributes": { "faction": "army", "rank": 5 } }{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }/external/<externalId>/attributesmembers:readCo przesłano dla konta zewnętrznego i kto je powiązał (discordId: null — jeszcze nikt).
curl "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }/external/<externalId>/attributesmembers:writeUsuń wszystkie atrybuty konta zewnętrznego.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeKod powiązania dla gracza: 8 znaków, jednorazowy, ważny przez ttlMinutes (1–60, domyślnie 15). Gracz klika „Powiąż konto” na panelu serwera (lub używa /link) i wpisuje kod. Nowy kod dla tego samego konta unieważnia poprzedni.
curl -X POST "https://api.echo-assistants.tech/link-codes" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }'{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }{ "code": "K7PQ3MZA", "display": "K7PQ-3MZA", "externalId": "Ivan_Petrov", "expiresAt": "2026-09-29T12:15:00.000Z" }/links?externalId=<externalId>links:readCzyje to konto: według externalId lub discordId (?discordId=…). Brak powiązania — 404 not_linked.
curl "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "externalId": "Ivan_Petrov", "discordId": "123...", "linkedAt": "2026-09-29T12:03:11.000Z" }/links?externalId=<externalId>links:writeOdłącz konto (lub ?discordId=…). Atrybuty konta zewnętrznego przestają działać na tę osobę.
curl -X DELETE "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123..." }/members/<discordId>/rolesroles:writeNadawaj i odbieraj role bezpośrednio — tylko te, które właściciel oznaczył w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy” (do 10 na każdej liście). remove odbiera każdą dozwoloną rolę, nawet nadaną ręcznie. Wykonuje to bot: odpowiedź 202 z numerem zadania.
durationMinutes (1–525 600) — nadaj role z add na określony czas: po jego upływie bot sam je zdejmie. Termin nigdy nie skraca już ustawionego, a rola, którą członek miał bezterminowo, zostaje na stałe. add bez durationMinutes nadaje rolę na stałe, remove anuluje zaplanowane zdjęcie.
curl -X POST "https://api.echo-assistants.tech/members/123456789012345678/roles" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }'{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }{ "ok": true, "opId": 42, "status": "pending" }/role-ops/<opId>roles:writeWynik zadania nadawania: pending, done lub failed oraz co się stało z każdą rolą.
Przy nadaniu na czas result.expiresAt pokazuje, kiedy bot zdejmie każdą nadaną rolę (null — członek miał już tę rolę bezterminowo i zostaje ona na stałe).
curl "https://api.echo-assistants.tech/role-ops/42" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "opId": 42, "discordId": "123...", "status": "done", "durationMinutes": 1440, "result": { "added": ["1369..."], "removed": [], "failed": [], "expiresAt": { "1369...": "2026-10-10T12:00:00.000Z" } } }/channels/<channelId>/messagesmessages:writeWiadomość w imieniu bota (własnego bota serwera, jeśli jest podłączony) — tylko w kanałach, które właściciel oznaczył w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy” (do 10). content — do 2000 znaków, embeds — do 10 prostych embedów: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; linki tylko https. Wzmianki domyślnie nikogo nie powiadamiają: powiadomić można tylko członków serwera z mentionUsers (do 10), a @everyone, @here i role nigdy. Treść żądania — do 32 KB. Wykonuje to bot: odpowiedź 202 z numerem zadania.
curl -X POST "https://api.echo-assistants.tech/channels/137100000000000000/messages" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }'{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }{ "ok": true, "opId": 77, "status": "pending" }/message-ops/<opId>messages:writeWynik wysyłki: pending, done (id wiadomości i link) albo failed z powodem: channel_not_allowed, channel_missing, missing_permissions, rejected.
curl "https://api.echo-assistants.tech/message-ops/42" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "opId": 77, "channelId": "1371...", "status": "done", "result": { "messageId": "1372...", "channelId": "1371...", "url": "https://discord.com/channels/1369.../1371.../1372..." } }Katalog
/catalog/servers?search=&tag=&lang=&limit=publiczny · bez kluczaWyszukiwanie i lista serwerów w katalogu monitoringu. Wszystko publiczne — klucz nie jest potrzebny.
curl "https://api.echo-assistants.tech/catalog/servers"
{ "count": 1, "servers": [ { "slug": "my-server", "name": "My Server", "memberCount": 5200, "flames": 980, "tags": ["Игры"], "language": "ru", "invite": "https://discord.gg/...", "badges": { "verified": true } } ] }/catalog/top?limit=<n>publiczny · bez kluczaTop katalogu według Płomieni.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>publiczny · bez kluczaPubliczna karta serwera: opis, Płomienie, miejsce, tagi, odznaki, zaproszenie, ocena.
curl "https://api.echo-assistants.tech/catalog/server/my-server"
{ "slug": "my-server", "name": "My Server", "flames": 980, "rank": 3, "rating": { "avg": 8.7, "count": 42 }, "invite": "https://discord.gg/..." }/catalog/reviews/<slug>?limit=<n>publiczny · bez kluczaOpinie o serwerze ze średnią oceną.
curl "https://api.echo-assistants.tech/catalog/reviews/my-server?limit=20"
{ "slug": "my-server", "summary": { "avg": 8.7, "count": 42 }, "reviews": [ { "user": "Ник", "rating": 9, "comment": "Топ", "at": "..." } ] }/catalog/tagspubliczny · bez kluczaTagi/kategorie katalogu z liczbą serwerów.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "tags": [ { "tag": "Игры", "count": 128 } ] }Webhooki
Echo samo wysyła zdarzenia na twój URL w czasie rzeczywistym. Skonfiguruj webhook w panelu → „Dla deweloperów”. Każde żądanie jest podpisane nagłówkiem X-Echo-Signature (HMAC-SHA256 treści z użyciem twojego sekretu), a nazwa zdarzenia jest w X-Echo-Event.
flame.bumpSerwer otrzymał Płomień — w katalogu lub komendą /flame.
{
"event": "flame.bump",
"guildId": "1369...",
"flames": 421,
"flamesTotal": 9101,
"amount": 1,
"source": "web",
"at": "2026-07-27T20:30:00.000Z"
}testWysyłane przyciskiem „Wyślij test” — by sprawdzić, czy odbiornik działa.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }import crypto from "node:crypto";
// body — surowa treść żądania, secret — z panelu
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(body).digest("hex");
const header = req.headers["x-echo-signature"];
const got = Buffer.from(typeof header === "string" ? header : "");
const want = Buffer.from(expected);
const ok = got.length === want.length && crypto.timingSafeEqual(got, want);Powiadomienia integracji (Premium)
Konfiguruje się je w panelu → „Integracje”. Echo wysyła POST z JSON-em {"id", "event", "guildId", "at", "data"}: podpis — X-Echo-Signature (sha256= i HMAC-SHA256 surowej treści z użyciem twojego sekretu), nazwa zdarzenia — X-Echo-Event, numer dostarczenia — X-Echo-Delivery.
Odpowiedz kodem 2xx w ciągu 8 sekund. W przeciwnym razie — ponowienie po 30 s, 2 min, 10 min i 1 godz. z tym samym id; przekierowania nie są obsługiwane. Adres — tylko https na porcie 443 i publiczne IP. Po 20 nieudanych próbach z rzędu wysyłanie zostaje wyłączone — włącz je w panelu, gdy odbiornik zacznie działać.
member.joinCzłonek dołączył do serwera.
{ "id": "evt_…", "event": "member.join", "guildId": "1369...", "at": "2026-09-29T12:00:00.000Z",
"data": { "discordId": "123...", "username": "ivan", "joinedAt": "2026-09-29T12:00:00.000Z" } }member.leaveCzłonek wyszedł lub został wyrzucony.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upCzłonek awansował na wyższy poziom.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseZakup w witrynie serwera: rola nadana, pakiet monet dodany (kind "coins", pole coins) lub wsparcie serwera. Anonimowe wsparcie nie ma kupującego.
{ "id": "evt_…", "event": "store.purchase", "guildId": "1369...", "at": "…",
"data": { "orderId": 815, "kind": "role", "itemName": "VIP", "roleId": "1369...", "buyerId": "123...", "discordId": "123...", "priceKopecks": 14900, "durationDays": 30 } }ticket.openedOtwarto ticket.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedGracz powiązał konto kodem.
{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
"data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }testPrzycisk „Wyślij test” w panelu → „Integracje”.
{ "id": "evt_…", "event": "test", "guildId": "1369...", "at": "…", "data": { "message": "…" } }import crypto from "node:crypto";
import express from "express";
import pg from "pg";
const app = express();
const SECRET = process.env.ECHO_WEBHOOK_SECRET; // whsec_… из панели
const GUILD_ID = process.env.ECHO_GUILD_ID;
if (!SECRET || !GUILD_ID) throw new Error("ECHO_WEBHOOK_SECRET and ECHO_GUILD_ID required");
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
// Выполните миграцию в своей PostgreSQL заранее:
// CREATE TABLE echo_webhook_events (
// guild_id text NOT NULL, event_id text NOT NULL,
// PRIMARY KEY (guild_id, event_id)
// );
// Храните ID минимум 2 часа. Несколько процессов используют одну таблицу.
// podpis liczony jest z surowej treści, przed parsowaniem JSON
app.post("/echo-webhook", express.raw({ type: "application/json", limit: "64kb" }), async (req, res) => {
if (!Buffer.isBuffer(req.body)) return res.status(400).end();
const expected = "sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const got = Buffer.from(req.get("X-Echo-Signature") ?? "");
const want = Buffer.from(expected);
const ok = got.length === want.length && crypto.timingSafeEqual(got, want);
if (!ok) return res.status(401).end();
let event;
try { event = JSON.parse(req.body.toString("utf8")); }
catch { return res.status(400).end(); }
if (!event || typeof event !== "object" || Array.isArray(event) ||
typeof event.id !== "string" || !/^evt_[0-9a-f-]{36}$/.test(event.id) ||
event.guildId !== GUILD_ID || typeof event.event !== "string" ||
typeof event.at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.at)) return res.status(400).end();
const at = Date.parse(event.at);
const age = Date.now() - at;
// Повторы сохраняют at; неверная дата и будущее дальше 5 минут — отказ.
if (!Number.isFinite(at) || new Date(at).toISOString() !== event.at || age > 2 * 60 * 60_000 || age < -5 * 60_000) return res.status(400).end();
let db;
try {
db = await pool.connect();
await db.query("BEGIN");
const claimed = await db.query(
"INSERT INTO echo_webhook_events (guild_id, event_id) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING event_id",
[event.guildId, event.id],
);
if (!claimed.rowCount) {
await db.query("ROLLBACK");
return res.status(204).end();
}
// Изменение награды/баланса делайте через db В ЭТОЙ транзакции.
// Для внешнего действия записывайте outbox здесь и обрабатывайте отдельно.
await db.query("COMMIT");
return res.status(204).end();
} catch {
if (db) await db.query("ROLLBACK").catch(() => {});
return res.status(500).end();
} finally {
db?.release();
}
});<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// podpis liczony jest z surowej treści, przed parsowaniem JSON
$body = file_get_contents('php://input');
if ($body === false || strlen($body) > 65536) { http_response_code(413); exit; }
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$got = $_SERVER['HTTP_X_ECHO_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
if (!is_array($event) || !is_string($event['id'] ?? null) ||
!preg_match('/^evt_[0-9a-f-]{36}$/D', $event['id']) ||
($event['guildId'] ?? null) !== $guildId ||
!is_string($event['event'] ?? null) || !is_string($event['at'] ?? null) ||
!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/D', $event['at'])) {
http_response_code(400);
exit;
}
$date = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z', $event['at'], new DateTimeZone('UTC'));
$at = $date && $date->format('Y-m-d\TH:i:s.v\Z') === $event['at'] ? $date->getTimestamp() : false;
$age = $at === false ? null : time() - $at;
if ($age === null || $age > 7200 || $age < -300) {
http_response_code(400); exit;
}
// Та же миграция PostgreSQL, что в Node.js-примере.
// ECHO_DATABASE_DSN: pgsql:host=…;dbname=… (PDO_PGSQL).
$db = new PDO(getenv('ECHO_DATABASE_DSN'), getenv('ECHO_DATABASE_USER'), getenv('ECHO_DATABASE_PASSWORD'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
try {
$db->beginTransaction();
$claim = $db->prepare('INSERT INTO echo_webhook_events (guild_id, event_id) VALUES (?, ?) ON CONFLICT DO NOTHING RETURNING event_id');
$claim->execute([$event['guildId'], $event['id']]);
if ($claim->fetchColumn() === false) {
$db->rollBack(); http_response_code(204); exit;
}
// Изменение награды/баланса делайте через $db В ЭТОЙ транзакции.
// Внешние действия — через outbox, записанный здесь.
$db->commit();
http_response_code(204);
} catch (Throwable $e) {
if ($db->inTransaction()) $db->rollBack();
http_response_code(500);
}Zakresy
Przy tworzeniu klucza wybierasz uprawnienia — każdy endpoint wymaga własnego zakresu.
guild:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeLimity i błędy
Do 3 (z Premium — 10) kluczy na serwer, limit żądań na minutę dla każdego klucza: 40 (z Premium — 120). Błędy są zwracane jako JSON { "error": "..." } z odpowiednim kodem HTTP.
| Kod | error | Kiedy |
|---|---|---|
| 400 | bad_fields | Nieprawidłowe lub brakujące parametry żądania. |
| 401 | unauthorized | Klucz nie został przekazany, jest nieprawidłowy lub unieważniony. |
| 403 | insufficient_scope | Klucz nie ma zakresu wymaganego dla tego żądania. |
| 403 | key_paused_premium | Klucz jest wstrzymany: serwer nie ma Premium, a bez niego działają tylko klucze w ramach darmowego limitu, wygenerowane jako pierwsze. |
| 404 | not_linked | Konto nie jest powiązane (metody powiązań) lub nie znaleziono zadania. |
| 409 | levels_off | Działanie nie pasuje do ustawień serwera: na przykład poziomy są wyłączone. |
| 413 | body_too_large | Treść żądania przekracza 8 KB. |
| 429 | rate_limited | Przekroczono limit żądań na minutę (40 (z Premium — 120)). |
| 502 | discord_unavailable | Discord nie odpowiedział — powtórz zapytanie później. |
Pełny klucz widać tylko raz, przy tworzeniu — zapisz go. Administracja projektu może wyłączyć klucz w razie nadużyć.