API Echo
Ашық REST API және вебхуктар: сервердің экономикасын, деңгейлерін және мониторингін өз сервистеріңе — ойын серверіне, сайтқа, ботқа немесе автоматтандыруға қос.
https://api.echo-assistants.tech
Авторизация
Әр сұрауға Authorization тақырыбында кілт керек. Кілт аккаунтқа емес, серверге беріледі. Оны басқару панелі → «Әзірлеушілерге» бөлімінде тек сервер иесі жасай алады: 3 (Premium-мен — 10) кілтке дейін. /catalog/* эндпоинттері ашық, кілт керек емес (IP бойынша минутына 60 шегі, жауаптар кэштеледі).
curl https://api.echo-assistants.tech/me \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Кілттер қауіпсіздігі
Кілт echoapi_ арқылы басталады, одан кейін 30 кездейсоқ латын әрпі мен цифр және бақылау сомасының 6 таңбасы келеді. Сомасы қате жолды API бірден қабылдамайды (401). Бұрынғы түрдегі кілттер (echo_…) жұмыс істей береді; қайта шығарғанда кілт жаңа түрге ауысады.
\bechoapi_[0-9A-Za-z]{36}\bЖария болған кілт автоматты түрде өшіріледі. GitHub Secret Scanning бағдарламасы арқылы бізге жария репозиторийлердегі кілттер туралы хабарлайды, ал Echo боты серверлердегі хабарламаларда кілттерді байқап, мұндай хабарламаларды жояды. Сервер иесі мен кілтті жасаған адам сайтта, Telegram-да және поштада хабарландыру алады — табылған жердің сілтемесімен.
Не істеу керек: әзірлеуші кабинетінде сервер кілттерін ашып, кілт карточкасындағы Қайта шығару түймесін басыңыз. Кілт жаңа құпия алып, қайта жұмыс істей бастайды, бұрынғысы бірден әрекет етуін тоқтатады. Кілтті жазылған барлық жерде ауыстырыңыз. Табылған кілтті қайта шығармай қосуға болмайды. Кілт енді керек болмаса, оны кері алыңыз.
Кілтті өз серверіңізде — орта айнымалыларында немесе құпиялар қоймасында сақтаңыз. Оны беттер мен қолданбалардың кодына, репозиторийлерге, хабарламаларға және скриншоттарға қоймаңыз.
Сервис
/meскоупсызКілтті тексеру: серверді және оның скоуптарын қайтарады.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Экономика
/economy/balance?user=<id>economy:readҚатысушы балансы: әмиян, банк, фишкалар.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writeВалюта қосу немесе шегеру. delta теріс болуы мүмкін; баланс 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:readБаланс бойынша үздік қатысушылар (limit 1–100, әдепкі 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 } ] }Деңгейлер
/levels/leaderboard?limit=<n>levels:readТәжірибе бойынша үздік қатысушылар (limit 1–100, әдепкі 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:readҚатысушының тәжірибесі мен деңгейі; nextLevelXp — келесі деңгейге барлығы қанша тәжірибе керек.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeТәжірибе қосу немесе шегеру: delta — бүтін сан, бір сұрауға ±1 000 000 дейін; тәжірибе нөлден төмен түспейді. Деңгей бірден қайта есептеледі. Егер ол өссе, бот әдеттегі деңгей көтерудегідей деңгей сыйлықтарын (рөлдер, монеталар, жарияланым) береді және "via": "api" белгісімен member.level_up оқиғасын жібереді. Деңгей төмендегенде сыйлықтар алынбайды. Тек сервер қатысушылары үшін (әйтпесе 404 not_member); серверде деңгейлер өшірулі болса — 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/servermonitoring:readКаталогтағы сервер карточкасы: кезеңдегі Жалын, барлығы, үздіктер тізіміндегі орны және күйлері.
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:readЖалын бойынша каталогтың үздік серверлері (limit 1–100, әдепкі 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 } ] }Интеграциялар
Өз ойыныңды, сайтыңды немесе CRM-ді қосу: қатысушылардың атрибуттары басқару панелі → «Интеграциялар» бөліміндегі ережелер бойынша рөлдерге айналады, ойыншылар аккаунттарын кодпен байланыстырады, рұқсат етілген рөлдерді тікелей беруге болады. Жазба денесі — 8 КБ-қа дейінгі JSON.
/members/<discordId>/attributesmembers:writeҚатысушы атрибуттарын беру (толығымен ауыстыру): 25 өріске дейін, атауы — латын әріптері/цифрлар/_, мәні — 100 таңбаға дейінгі жол, сан немесе true/false. Ережелер бойынша рөлдерді бот бірден қайта есептейді.
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:readҚатысушы бойынша не жіберілгені және ол қай сыртқы аккаунтты байланыстырғаны.
timedRoles — API үшін рұқсат етілгендер ішінен мерзімге берілген рөлдер және бот оларды қашан алатыны.
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:writeҚатысушының барлық атрибуттарын алып тастау. Ереже берген рөлдерді бот алады; қолмен берілгендеріне тиіспейді.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeДәл солай, бірақ жүйеңдегі ID бойынша (ник, UUID, license:…). Аккаунтты кодпен байланыстырған адамға әсер етеді; әзірге байланыстырылмаса — сақталады да, байланыстырғанда қолданылады. Атауы сәйкес келсе, 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:readСыртқы аккаунт бойынша не жіберілгені және оны кім байланыстырғаны (discordId: null — әзірге ешкім).
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:writeСыртқы аккаунттың барлық атрибуттарын алып тастау.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeОйыншыға байланыстыру коды: 8 таңба, бір реттік, ttlMinutes бойы жарамды (1–60, әдепкі 15). Ойыншы сервер панеліндегі «Аккаунтты байланыстыру» түймесін басады (немесе /link) да, кодты енгізеді. Сол аккаунттың жаңа коды алдыңғысын жояды.
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:readКімнің аккаунты: externalId немесе discordId бойынша (?discordId=…). Байланыс жоқ болса — 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:writeАккаунтты ажырату (немесе ?discordId=…). Сыртқы аккаунттың атрибуттары адамға әсер етуін тоқтатады.
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:writeРөлдерді тікелей беру және алу — тек ие әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде белгілегендерін (әр тізімде 10-ға дейін). remove рұқсат етілген кез келген рөлді, тіпті қолмен берілгенін де алады. Ботпен орындалады: тапсырма нөмірімен 202 жауабы.
durationMinutes (1–525 600) — add ішіндегі рөлдерді мерзімге беру: мерзімі біткенде бот оларды өзі алып тастайды. Мерзім бұрын тағайындалғанын қысқартпайды, ал қатысушыда мерзімсіз болған рөл мерзімсіз күйінде қалады. durationMinutes жоқ add рөлді мерзімсіз етеді, remove тағайындалған алып тастауды болдырмайды.
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:writeРөл беру тапсырмасының нәтижесі: pending, done немесе failed және әр рөл бойынша не шыққаны.
Мерзімге берілгенде result.expiresAt ішінде бот әр берілген рөлді қашан алатыны көрсетіледі (null — рөл қатысушыда мерзімсіз болған және мерзімсіз қалды).
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:writeБот атынан хабарлама (қосылған болса, сервердің өз боты) — тек ие әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде белгілеген арналарға (10-ға дейін). content — 2000 таңбаға дейін, embeds — 10 қарапайым эмбедке дейін: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; сілтемелер тек https. Әдепкі бойынша аталымдар ешкімді шақырмайды: тек mentionUsers тізіміндегі сервер қатысушыларын (10-ға дейін) шақыруға болады, ал @everyone, @here және рөлдер ешқашан шақырылмайды. Сұрау денесі — 32 КБ-қа дейін. Ботпен орындалады: тапсырма нөмірімен 202 жауабы.
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:writeЖіберу нәтижесі: pending, done (хабарлама id-і және сілтеме) немесе себебі көрсетілген failed: 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..." } }Каталог
/catalog/servers?search=&tag=&lang=&limit=ашық · кілтсізМониторинг каталогындағы серверлерді іздеу және тізімі. Барлығы ашық — кілт керек емес.
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>ашық · кілтсізЖалын бойынша каталог үздіктері.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>ашық · кілтсізСервердің ашық карточкасы: сипаттама, Жалын, орны, тегтер, белгілер, шақыру, рейтинг.
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>ашық · кілтсізСервер туралы пікірлер және орташа баға.
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/tagsашық · кілтсізКаталог тегтері/санаттары және ондағы серверлер саны.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "tags": [ { "tag": "Игры", "count": 128 } ] }Вебхуктар
Echo оқиғаларды URL-іңе нақты уақытта өзі жібереді. Вебхукты басқару панелі → «Әзірлеушілерге» бөлімінде бапта. Әр сұрау X-Echo-Signature тақырыбымен қол қойылған (құпия кілтің бойынша дененің HMAC-SHA256 мәні), оқиға атауы — X-Echo-Event ішінде.
flame.bumpСерверге каталогта немесе /flame командасымен Жалын берілді.
{
"event": "flame.bump",
"guildId": "1369...",
"flames": 421,
"flamesTotal": 9101,
"amount": 1,
"source": "web",
"at": "2026-07-27T20:30:00.000Z"
}test«Тест жіберу» түймесімен жіберіледі — қабылдағыштың жұмыс істейтінін тексеру үшін.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }import crypto from "node:crypto";
// body — сұраудың өңделмеген мәтіні, secret — басқару панелінен
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);Интеграция хабарландырулары (Premium)
Басқару панелі → «Интеграциялар» бөлімінде бапталады. Echo JSON-мен POST жібереді {"id", "event", "guildId", "at", "data"}: қолтаңба — X-Echo-Signature (sha256= және құпияң бойынша шикі дененің HMAC-SHA256-сы), оқиға атауы — X-Echo-Event, жеткізу нөмірі — X-Echo-Delivery.
8 секунд ішінде 2xx жауап бер. Әйтпесе — сол id-мен 30 с, 2 мин, 10 мин және 1 сағаттан кейін қайталау; қайта бағыттаулар орындалмайды. Мекенжай — тек 443 портындағы https және жария IP. Қатарынан 20 сәтсіздіктен кейін жіберу өшіріледі — қабылдағыш жұмыс істей бастағанда, оны басқару панелінде қос.
member.joinҚатысушы серверге қосылды.
{ "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.leaveҚатысушы шықты немесе оны шығарып жіберді.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upҚатысушы деңгейін көтерді.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseСервер витринасында сатып алу: рөл берілді, монета пакеті есептелді (kind "coins", coins өрісі) немесе серверге қолдау. Анонимді қолдаудың сатып алушысы жоқ.
{ "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.openedТикет ашылды.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedОйыншы аккаунтты кодпен байланыстырды.
{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
"data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }testБасқару панелі → «Интеграциялар» бөліміндегі «Тест жіберу» түймесі.
{ "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 часа. Несколько процессов используют одну таблицу.
// қолтаңба 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; }
// қолтаңба 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);
}Скоуптар
Кілт жасағанда құқықтар таңдалады — әр эндпоинт өз скоупын талап етеді.
guild:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeШектер мен қателер
Бір серверге 3 (Premium-мен — 10) кілтке дейін, шек — әр кілтке минутына 40 (Premium-мен — 120) сұрау. Қателер тиісті HTTP-кодпен JSON { "error": "..." } түрінде келеді.
| Код | error | Қашан |
|---|---|---|
| 400 | bad_fields | Сұрау параметрлері қате немесе жетіспейді. |
| 401 | unauthorized | Кілт берілмеген, қате немесе кері қайтарылған. |
| 403 | insufficient_scope | Кілтте бұл сұрауға қажетті скоуп жоқ. |
| 403 | key_paused_premium | Кілт кідіртілген: серверде Premium жоқ, ал онсыз тек тегін шек ішіндегі, басқалардан бұрын шығарылған кілттер жұмыс істейді. |
| 404 | not_linked | Аккаунт байланыстырылмаған (байланыс әдістері) немесе тапсырма табылмады. |
| 409 | levels_off | Әрекет сервер баптауларына сай келмейді: мысалы, деңгейлер өшірулі. |
| 413 | body_too_large | Сұрау денесі 8 КБ-тан үлкен. |
| 429 | rate_limited | Минутына 40 (Premium-мен — 120) сұрау шегінен асты. |
| 502 | discord_unavailable | Discord жауап бермеді — сұрауды кейінірек қайталаңыз. |
Кілт толық күйінде тек жасалған кезде бір рет көрінеді — оны сақтап қой. Теріс пайдаланылса, жоба әкімшілігі кілтті өшіре алады.