API Echo

واجهة REST API عامة وWebhooks: يمكن ربط اقتصاد الخادم ومستوياته وبيانات دليل الخوادم بخدماتك الخاصة — خادم ألعاب أو موقع أو بوت أو أتمتة.

https://api.echo-assistants.tech

التفويض

يحتاج كل طلب إلى مفتاح في الترويسة Authorization. المفتاح يخص الخادم وليس الحساب. لا ينشئه إلا مالك الخادم في لوحة التحكم ← «المطوّرون»: بحد أقصى 3 (10 مع Premium) مفاتيح. نقاط النهاية /catalog/* عامة ولا تحتاج إلى مفتاح (الحد 60 طلبًا في الدقيقة لكل عنوان IP، والردود مخزّنة مؤقتًا).

curl https://api.echo-assistants.tech/me \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
إنشاء مفتاح

أمان المفاتيح

يبدأ المفتاح بـ echoapi_، يليه 30 رمزًا عشوائيًا من الحروف اللاتينية والأرقام، ثم مجموع تحقق من 6 رموز. ترفض API فورًا (401) أي سلسلة مجموع تحققها خاطئ. مفاتيح الصيغة القديمة (echo_…) تبقى صالحة، والمفتاح المعاد إصداره يحصل على الصيغة الجديدة.

تعبير للبحث عن المفاتيح في شيفرتك
\bechoapi_[0-9A-Za-z]{36}\b

يُعطَّل المفتاح المكشوف علنًا تلقائيًا. عبر برنامج Secret Scanning يبلغنا GitHub بالمفاتيح الموجودة في المستودعات العامة، ويرصد بوت Echo المفاتيح في رسائل الخوادم ويحذف تلك الرسائل. يتلقى مالك الخادم ومنشئ المفتاح إشعارًا على الموقع وفي Telegram وبالبريد الإلكتروني، مع رابط إلى مكان العثور على المفتاح.

ما العمل: فتح مفاتيح الخادم في بوابة المطوّرين والضغط على إعادة الإصدار في بطاقة المفتاح. يحصل المفتاح على سرّ جديد ويعمل مجددًا، ويتوقف السابق فورًا. يجب استبدال المفتاح في كل مكان يُحفظ فيه. لا يمكن تفعيل مفتاح عُثر عليه دون إعادة إصداره. إذا لم يعد المفتاح لازمًا فيمكن إبطاله.

يُحفظ المفتاح على خادمك: في متغيرات البيئة أو مخزن الأسرار. لا يجوز وضعه في شيفرة الصفحات أو التطبيقات أو في المستودعات أو الرسائل أو لقطات الشاشة.

الخدمة

GET/meبدون نطاق

التحقق من المفتاح: يعيد الخادم ونطاقاته.

مثال
curl "https://api.echo-assistants.tech/me" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
الاستجابة
{ "guildId": "1369...", "scopes": ["levels:read"] }

الاقتصاد

GET/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 }
POST/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 }
GET/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 } ] }

المستويات

GET/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 } ] }
GET/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 }
POST/levels/xplevels:write

إضافة نقاط الخبرة أو خصمها: delta عدد صحيح، حتى ±1,000,000 لكل طلب، ولا تنزل نقاط الخبرة تحت الصفر. يُعاد حساب المستوى فورًا. إذا ارتفع المستوى، يمنح البوت مكافآته (الرتب والعملات والإعلان) كما في الترقية العادية، ويرسل الحدث member.level_up مع العلامة "via": "api". خفض المستوى لا يسحب المكافآت. لأعضاء الخادم فقط (وإلا 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 }

دليل الخوادم

GET/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 }
GET/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: تتحول سمات الأعضاء إلى رتب وفق القواعد في لوحة التحكم ← «التكاملات»، ويربط اللاعبون حساباتهم برمز، ويمكن منح الرتب المسموح بها مباشرة. أجسام طلبات الكتابة بصيغة JSON بحجم حتى 8 كيلوبايت.

PUT/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 } }
GET/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" } ] }
DELETE/members/<discordId>/attributesmembers:write

مسح كل سمات العضو. يسحب البوت الرتب التي منحتها القواعد، ولا يمس الرتب الممنوحة يدويًا.

مثال
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
الاستجابة
{ "ok": true, "removed": true }
PUT/external/<externalId>/attributesmembers:write

الأمر نفسه حسب المعرّف في نظامك (الاسم المستعار، UUID، license:…). يسري على من ربط الحساب برمز، وقبل ذلك يُحفظ ويُطبَّق عند الربط. عند تطابق الأسماء تكون الأولوية للسمات المرسلة حسب معرّف Discord.

مثال
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 } }
GET/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 } }
DELETE/external/<externalId>/attributesmembers:write

مسح كل سمات الحساب الخارجي.

مثال
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
الاستجابة
{ "ok": true, "removed": true }
POST/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" }
GET/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" }
DELETE/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..." }
POST/members/<discordId>/rolesroles:write

منح الرتب وسحبها مباشرة، وذلك للرتب التي حددها المالك في بوابة المطوّرين ← «مفاتيح API» ← «مسموح للمفاتيح» فقط (حتى 10 في كل قائمة). يسحب remove أي رتبة مسموح بها، حتى الممنوحة يدويًا. ينفّذ البوت ذلك: الرد 202 مع رقم المهمة.

durationMinutes (من 1 إلى 525,600): منح الرتب من add لمدة محددة، وعند انتهائها يسحبها البوت تلقائيًا. لا تُقصِّر المدة الجديدة مدة محددة مسبقًا، والرتبة التي كانت لدى العضو بلا مدة تبقى دائمة. add بدون durationMinutes يجعل الرتبة دائمة، و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" }
GET/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" } } }
POST/channels/<channelId>/messagesmessages:write

رسالة باسم البوت (البوت الخاص بالخادم إن كان مربوطًا)، وذلك فقط إلى القنوات التي حددها المالك في بوابة المطوّرين ← «مفاتيح API» ← «مسموح للمفاتيح» (حتى 10). content حتى 2000 حرف، وembeds حتى 10 عناصر Embed بسيطة: 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" }
GET/message-ops/<opId>messages:write

نتيجة الإرسال: pending، أو done (معرّف الرسالة ورابطها)، أو 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..." } }

الدليل العام

GET/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 } } ] }
GET/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 } ] }
GET/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/..." }
GET/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": "..." } ] }
GET/catalog/tagsعام · بدون مفتاح

وسوم الدليل وفئاته مع عدد الخوادم في كل منها.

مثال
curl "https://api.echo-assistants.tech/catalog/tags"
الاستجابة
{ "tags": [ { "tag": "Игры", "count": 128 } ] }

Webhooks

يرسل Echo الأحداث إلى عنوان URL الخاص بك في الوقت الفعلي. يُضبط Webhook في لوحة التحكم ← «المطوّرون». كل طلب موقّع بالترويسة 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 طلب POST بصيغة JSON {"id", "event", "guildId", "at", "data"}: التوقيع في X-Echo-Signature (sha256= ثم HMAC-SHA256 للجسم الخام باستخدام المفتاح السري)، واسم الحدث في X-Echo-Event، ورقم التسليم في X-Echo-Delivery.

يجب الرد بـ 2xx خلال 8 ثوانٍ. وإلا نعيد المحاولة بعد 30 ثانية ودقيقتين و10 دقائق وساعة بالمعرّف id نفسه، ولا تُتَّبع عمليات إعادة التوجيه. يجب أن يكون العنوان https على المنفذ 443 وبعنوان 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": "…" } }
التحقق من التوقيع — Node.js
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
<?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:read
بيانات الخادم
معلومات أساسية عن الخادم.
economy:read
قراءة الاقتصاد
الأرصدة: المحفظة والبنك والرقائق.
economy:write
الكتابة في الاقتصاد
إضافة العملة وخصمها.
levels:read
قراءة المستويات
نقاط الخبرة وترتيب الأعضاء.
monitoring:read
قراءة دليل الخوادم
اللهب والمركز في الترتيب وحالات الدليل.
members:read
قراءة السمات
قراءة سمات الأعضاء حسب معرّف Discord وحسب المعرّف في النظام الخارجي، دون صلاحية تغييرها.
members:write
سمات الأعضاء
إرسال السمات حسب معرّف Discord وحسب المعرّف في النظام الخارجي.
roles:write
منح الرتب
منح الرتب المسموح بها في بوابة المطوّرين ← «مفاتيح API» ← «مسموح للمفاتيح» وسحبها.
links:read
قراءة الربط
صاحب الحساب: الربط بين المعرّف الخارجي وDiscord.
links:write
رموز الربط
إصدار رموز الربط وفك ربط الحسابات.
levels:write
الكتابة في نقاط الخبرة
إضافة نقاط الخبرة وخصمها، ويمنح البوت مكافآت المستويات.
messages:write
رسائل البوت
الرسائل باسم البوت في القنوات المسموح بها في بوابة المطوّرين ← «مفاتيح API» ← «مسموح للمفاتيح».

الحدود والأخطاء

بحد أقصى 3 (10 مع Premium) من المفاتيح لكل خادم، والحد 40 (120 مع Premium) من الطلبات في الدقيقة لكل مفتاح. تُعاد الأخطاء بصيغة JSON { "error": "..." } مع رمز HTTP المناسب.

الرمزerrorمتى
400bad_fieldsمعاملات الطلب غير صحيحة أو ناقصة.
401unauthorizedلم يُرسَل المفتاح، أو أنه غير صحيح أو مُبطَل.
403insufficient_scopeليس لدى المفتاح النطاق المطلوب لهذا الطلب.
403key_paused_premiumالمفتاح متوقف مؤقتًا: لا يوجد Premium للخادم، وبدونه تعمل فقط المفاتيح الأقدم إصدارًا ضمن الحد المجاني.
404not_linkedالحساب غير مربوط (طرق الربط) أو لم يُعثر على المهمة.
409levels_offالإجراء لا يتوافق مع إعدادات الخادم: مثلًا، المستويات معطّلة.
413body_too_largeجسم الطلب أكبر من 8 كيلوبايت.
429rate_limitedتم تجاوز حد الطلبات في الدقيقة (40 (120 مع Premium)).
502discord_unavailableلم يستجب Discord. يُرجى إعادة المحاولة لاحقًا.

يظهر المفتاح كاملًا مرة واحدة فقط عند إنشائه، لذا يجب حفظه. قد تعطّل إدارة المشروع المفتاح عند إساءة استخدامه.

لا تنسَ حفظ التغييرات.
توجد تغييرات غير محفوظة — تطبيقها أم إلغاؤها؟