# أمثلة برمجية

> برامج كاملة على Node.js وPython لواجهة Fileverity API: فحص صورة، وفحص ملف PDF كبير، وكشف النص المكتوب بالذكاء الاصطناعي، والتحقق من توقيعات Webhook.

المصدر: https://fileverity.com/ar/docs/sample-code

تضم هذه الصفحة أربعة برامج كاملة لواجهة Fileverity API، يعمل كل منها على Node.js (الإصدار 18 أو أحدث) وعلى Python (الإصدار 3.9 أو أحدث). وهي: فحص صورة وانتظار النتيجة، وفحص ملف PDF كبير، وكشف النص المكتوب بالذكاء الاصطناعي، واستقبال Webhooks مع التحقق من التوقيع. انسخ أحدها، وعيّن `FILEVERITY_API_KEY`، ثم شغّله.

تستدعي البرامج API عبر HTTPS مباشرة، باستخدام `fetch` المضمّنة في Node.js وحزمة `requests` في Python، فلا تحتاج إلا إلى عميل HTTP. والاستدعاءات والحقول والحالات هي نفسها الموصوفة في [الملفات](https://fileverity.com/ar/docs/files.md) و[التحليلات](https://fileverity.com/ar/docs/analyses.md) و[كشف النصوص](https://fileverity.com/ar/docs/text-detections.md) و[Webhooks](https://fileverity.com/ar/docs/webhooks.md).

| البرنامج | ما يفعله | الاستدعاءات |
| --- | --- | --- |
| [فحص صورة](https://fileverity.com/ar/docs/sample-code.md#image) | يرفع صورة وينتظر النتيجة ثم يطبع الملخص وعنوان URL للتقرير. | `POST /files`, `POST /analyses`, `GET /analyses/{id}` |
| [فحص ملف PDF كبير](https://fileverity.com/ar/docs/sample-code.md#pdf) | يرفع مستندًا عبر عنوان URL موقَّع مسبقًا، ويكرر الاستعلام حتى ينتهي الفحص، ثم يحفظ تقرير PDF. | `POST /files`, `GET /files/{id}`, `POST /analyses`, `GET /analyses/{id}`, `GET /analyses/{id}/report.pdf` |
| [كشف النص المكتوب بالذكاء الاصطناعي](https://fileverity.com/ar/docs/sample-code.md#text) | يرسل ملفًا نصيًا ويطبع الحكم والدرجة وكل فقرة مكتوبة بالذكاء الاصطناعي. | `POST /text/detections` |
| [استقبال Webhooks](https://fileverity.com/ar/docs/sample-code.md#webhooks) | يتحقق من توقيع كل عملية تسليم، ويتجاهل التكرارات، ويعالج الأحداث. | نقطة النهاية لديك، التي تُستدعى بالأحداث `analysis.completed` و`analysis.failed` و`balance.low` |

## قبل التشغيل

- أنشئ مفتاحًا من نوع **اختبار** ضمن [مفاتيح API](https://fileverity.com/console/keys?lang=ar) في لوحة التحكم. يعيد مفتاح الاختبار نتائج نموذجية ثابتة ولا تُحتسب عليه رسوم أبدًا، فيعمل كل برنامج دون أي تكلفة. ولفحص ملفات حقيقية، أنشئ مفتاحًا من نوع **حقيقي**. ولا تتغير البرامج.
- Node.js: احفظ كل برنامج بوصفه ملف `.mjs`، فيتيح لك استخدام `import` و`await` على المستوى الأعلى، ثم شغّله بالأمر `node`. لا يلزم تثبيت أي شيء.
- Python: شغّل `pip install requests`. ويحتاج مستقبِل Webhook أيضًا إلى `pip install flask`.

**تعيين المفتاح: Shell**

```bash
export FILEVERITY_API_KEY=fv_test_...
```

**تعيين المفتاح: PowerShell**

```powershell
$env:FILEVERITY_API_KEY = "fv_test_..."
```

## كيف تتعامل البرامج مع المفاتيح والأخطاء وإعادة المحاولة

تشترك البرامج الثلاثة التي تستدعي API في الدالتين `request` و`api` نفسيهما. انسخهما إلى شيفرتك كما هما.

| الجانب | ما تفعله البرامج |
| --- | --- |
| المفتاح | يُقرأ من `FILEVERITY_API_KEY` ولا يُكتب أبدًا داخل الشيفرة. استدعِ API من خادمك، لا من شيفرة المتصفح. |
| Idempotency | يرسل كل طلب POST الترويسة `Idempotency-Key` بقيمة UUID جديدة لكل طلب، ويعيد استخدامها في كل إعادة محاولة، فلا يُنفَّذ الفحص ولا يُحتسب سعره مرتين عند إعادة المحاولة. ولكي تكون إعادة تشغيل السكربت بالكامل آمنة، اشتق المفتاح من معرّف السجل لديك بدلًا من ذلك. راجع [Idempotency](https://fileverity.com/ar/docs/idempotency.md). |
| الأخطاء | تحمل الاستجابة التي ليست `2xx` الحقول `error.type` و`error.code` و`error.message` و`error.param` و`error.request_id`. وتتوقف البرامج مع الرسالة `code: message (request request_id)`. اعتمد على `code` في معالجة الخطأ، واذكر معرّف الطلب عند التواصل مع الدعم. راجع [الأخطاء](https://fileverity.com/ar/docs/errors.md). |
| إعادة المحاولة | تُعاد المحاولة عند تعطّل الشبكة، وعند أي حالة `5xx` (`internal_error` و`analysis_failed` و`service_unavailable`)، وعند `rate_limited` (رمز الحالة 429) و`idempotency_in_progress` (رمز الحالة 409)، بحد أقصى 5 محاولات. ومدة الانتظار هي قيمة الترويسة `retry-after` إذا كانت الاستجابة تحمل واحدة، وإلا فهي 2 ثم 4 ثم 8 ثم 16 ثانية. ويتوقف البرنامج فورًا عند أي حالة `4xx` أخرى. أما `pending_files_limit` فهي أيضًا حالة 429 لكنها لا تحمل `retry-after`، لذا لا تُعاد المحاولة معها. |
| حدّ المعدّل | عندما تُبلغ الاستجابة بالقيمة `x-ratelimit-remaining-requests: 0`، ينتظر البرنامج عدد الثواني الوارد في `x-ratelimit-reset-requests` قبل الاستدعاء التالي، حتى لا يصطدم بالخطأ 429. راجع [حدود المعدّل والترويسات](https://fileverity.com/ar/docs/rate-limits.md#headers). |
| المهلات | تنتهي مهلة برامج Python بعد 90 ثانية لكل طلب، وهي أطول من 60 ثانية، أقصى مدة يستطيع `Prefer: wait` إبقاء الطلب مفتوحًا خلالها. |

## فحص صورة وانتظار النتيجة

يرفع البرنامج الصورة بصيغة multipart، وينشئ تحليلًا مع `Prefer: wait=60` ليبقى الطلب مفتوحًا حتى تجهز النتيجة، ثم يطبع الملخص وعنوان URL للتقرير. فإذا انتهت مدة الانتظار أولًا، ترد API بالاستجابة `202` مع `status: processing`، ويكرر البرنامج استدعاء `GET /analyses/{id}` كل 3 ثوانٍ. راجع [الانتظار أو الاستعلام المتكرر أو استخدام Webhooks](https://fileverity.com/ar/docs/analyses.md#wait).

احفظه باسم `check-image.mjs` أو `check_image.py` وشغّل `node check-image.mjs photo.jpg` أو `python check_image.py photo.jpg`. يقبل multipart صورًا حتى 4 ميغابايت. أما الصورة الأكبر، حتى 10 ميغابايت، فاستخدم الرفع بعنوان موقّع مسبقًا كما في البرنامج التالي، مع نوع محتوى الصورة، مثل `image/jpeg`.

**فحص صورة: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-image.mjs photo.jpg");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const form = new FormData();
form.append("file", new Blob([await readFile(path)]), basename(path));
const file = await api("POST", "/files", {
  headers: { "Idempotency-Key": randomUUID() },
  body: form,
});

let analysis = await api("POST", "/analyses", {
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({ model: "fvimg-2", file: file.id }),
});
while (analysis.status === "processing") {
  await sleep(3);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, assessment, ai, edit } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, assessment ${assessment}`);
console.log(`AI generation: ${ai}, editing: ${edit}`);
console.log(`Report: ${analysis.report_url}`);
```

**فحص صورة: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_image.py photo.jpg")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
upload = {"file": (os.path.basename(sys.argv[1]), data)}
file = api("POST", "/files", headers={"Idempotency-Key": str(uuid.uuid4())}, files=upload)

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4()), "Prefer": "wait=60"},
    json={"model": "fvimg-2", "file": file["id"]},
)
while analysis["status"] == "processing":
    time.sleep(3)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, assessment {summary['assessment']}")
print(f"AI generation: {summary['ai']}, editing: {summary['edit']}")
print(f"Report: {analysis['report_url']}")
```

**مخرجات فحص الصورة**

```text
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded
level L2, action accept, assessment authentic_untouched
AI generation: very_low, editing: none
Report: https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

`summary` هو الحكم الثابت القابل للقراءة آليًا لفحص الصورة: `level` و`action` و`assessment` و`ai` و`edit`، كما هو موصوف في [ملخص الصورة](https://fileverity.com/ar/docs/analyses.md#summary-image). و`report_url` هو عنوان صفحة التحقق العامة للتقرير. وعند استخدام مفتاح اختبار تكون النتيجة نموذجية ثابتة، ولا توجد صفحة خلف عنوان URL للتقرير.

## فحص ملف PDF كبير

تُرفع المستندات حتى 50 ميغابايت على ثلاث خطوات: اطلب عنوان رفع عبر `POST /files` مع محتوى طلب بصيغة JSON، ثم أرسل البايتات إلى ذلك العنوان باستخدام `PUT`، ثم انتظر حتى تظهر حالة الملف `uploaded`. بعدها ينشئ البرنامج التحليل ويكرر الاستعلام كل 5 ثوانٍ، لأن المستندات قد تدخل في قائمة انتظار وقد يستغرق الفحص بعض الوقت. ويطبع التقدم والملخص وعنوان URL للتقرير، ويحفظ تقرير PDF بجوار السكربت.

احفظه باسم `check-pdf.mjs` أو `check_pdf.py` وشغّل `node check-pdf.mjs contract.pdf` أو `python check_pdf.py contract.pdf`. ولنوع مستند آخر، غيّر `content_type` إلى أحد [أنواع المحتوى المقبولة](https://fileverity.com/ar/docs/files.md#content-types). وللمستند المشفَّر، أضف `options: { password }` إلى طلب التحليل.

**فحص ملف PDF كبير: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-pdf.mjs contract.pdf");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const bytes = await readFile(path);
const file = await api("POST", "/files", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    filename: basename(path),
    content_type: "application/pdf",
    bytes: bytes.length,
  }),
});

if (file.status === "pending") {
  const { url, method, headers } = file.upload;
  await request(url, { method, headers, body: bytes });
  let ready = false;
  for (let attempt = 0; attempt < 15 && !ready; attempt++) {
    ready = (await api("GET", `/files/${file.id}`)).status === "uploaded";
    if (!ready) await sleep(2);
  }
  if (!ready) throw new Error("The upload was not confirmed.");
}
console.log(`${file.id} uploaded`);

let analysis = await api("POST", "/analyses", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ model: "fvdoc-3", file: file.id }),
});
const deadline = Date.now() + 15 * 60 * 1000;
while (analysis.status === "processing") {
  if (Date.now() > deadline) {
    throw new Error(`Still processing. Fetch it later with GET /analyses/${analysis.id}`);
  }
  console.log(`${analysis.id} processing: ${analysis.progress?.stage ?? "starting"}`);
  await sleep(5);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, reason, pages, images, text, findings } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, reason ${reason ?? "none"}`);
console.log(`${pages} pages, ${images} photos examined, text: ${text ?? "not evaluated"}`);
console.log(`findings: ${findings.join(", ") || "none"}`);
console.log(`Report: ${analysis.report_url}`);

if (analysis.livemode) {
  const pdf = await request(`${base}/analyses/${analysis.id}/report.pdf`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  const name = `Fileverity-${analysis.report_id}.pdf`;
  await writeFile(name, Buffer.from(await pdf.arrayBuffer()));
  console.log(`Saved ${name}`);
}
```

**فحص ملف PDF كبير: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_pdf.py contract.pdf")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
file = api(
    "POST",
    "/files",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "filename": os.path.basename(sys.argv[1]),
        "content_type": "application/pdf",
        "bytes": len(data),
    },
)

if file["status"] == "pending":
    request("PUT", file["upload"]["url"], headers=file["upload"]["headers"], data=data)
    for _ in range(15):
        if api("GET", f"/files/{file['id']}")["status"] == "uploaded":
            break
        time.sleep(2)
    else:
        sys.exit("The upload was not confirmed.")
print(f"{file['id']} uploaded")

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"model": "fvdoc-3", "file": file["id"]},
)
deadline = time.time() + 15 * 60
while analysis["status"] == "processing":
    if time.time() > deadline:
        sys.exit(f"Still processing. Fetch it later with GET /analyses/{analysis['id']}")
    stage = (analysis["progress"] or {}).get("stage", "starting")
    print(f"{analysis['id']} processing: {stage}")
    time.sleep(5)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
reason = summary["reason"] or "none"
text = summary["text"] or "not evaluated"
findings = ", ".join(summary["findings"]) or "none"
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, reason {reason}")
print(f"{summary['pages']} pages, {summary['images']} photos examined, text: {text}")
print(f"findings: {findings}")
print(f"Report: {analysis['report_url']}")

if analysis["livemode"]:
    url = f"{BASE}/analyses/{analysis['id']}/report.pdf"
    pdf = request("GET", url, headers={"Authorization": f"Bearer {KEY}"})
    name = f"Fileverity-{analysis['report_id']}.pdf"
    with open(name, "wb") as target:
        target.write(pdf.content)
    print(f"Saved {name}")
```

**مخرجات فحص PDF**

```text
file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploaded
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queued
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzing
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reporting
an_Zc4vB7nP1sK8tL3wR6yQm2Xa succeeded
level L2, action accept, reason clean
12 pages, 3 photos examined, text: human
findings: none
Report: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDC
Saved Fileverity-FV-3PXH-6WQ2-8NDC.pdf
```

- يذهب طلب `PUT` مباشرة إلى التخزين. ولا يحتاج إلى ترويسة `Authorization`، وتُرسَل الترويسات الموجودة في `upload.headers` كما أُعيدت تمامًا. راجع [الرفع بعنوان موقّع مسبقًا](https://fileverity.com/ar/docs/files.md#presigned).
- مع مفتاح الاختبار يعود الملف بالحالة `uploaded` ولا يوجد شيء يُرسَل، لذا يتخطى البرنامج الرفع. ويعيد التحليل نتيجة نموذجية ثابتة فورًا، ولا يوجد ملف PDF نموذجي، لذا لا يحفظ البرنامج التقرير إلا مع مفتاح حقيقي.
- نزّل ملف PDF فور انتهاء الفحص. تُحفظ صور صفحات المستند ومقتطفات نصه مدة 3 ساعات، وسيخلو ملف PDF الذي يُنزَّل بعد ذلك منها. راجع [تقرير PDF](https://fileverity.com/ar/docs/analyses.md#pdf).
- الفحص الذي ينتهي بالحالة `failed` أو `canceled` لا تُحتسب عليه رسوم. ويتوقف البرنامج مع رسالة الخطأ الخاصة بالتحليل.

## كشف النص المكتوب بالذكاء الاصطناعي

يتطلب كشف النص استدعاءً متزامنًا واحدًا. يقرأ البرنامج ملفًا نصيًا ويرسله إلى `POST /text/detections` ويطبع الحكم والدرجة ونسبة النص المكتوب بالذكاء الاصطناعي وكل فقرة مكتوبة بالذكاء الاصطناعي مع إزاحاتها. يدعم كشف النص المكتوب بالذكاء الاصطناعي اللغات الإنجليزية والفرنسية والألمانية والإسبانية والصينية (`en` و`fr` و`de` و`es` و`zh`). وتُكتشف اللغة تلقائيًا؛ مرّر رمز لغة بوصفه الوسيط الثاني لتحديدها. يجب ألا يقل النص عن 100 كلمة وألا يزيد على 100,000 حرف.

احفظه باسم `detect-text.mjs` أو `detect_text.py` وشغّل `node detect-text.mjs essay.txt` أو `python detect_text.py essay.txt fr`. ويتضمن [البدء السريع](https://fileverity.com/ar/docs/quickstart.md#text) أمرًا يكتب ملف `essay.txt` نموذجيًا. ومع مفتاح الاختبار تكون الاستجابة نتيجة ثابتة لنص كتبه إنسان، لذا لا يطبع البرنامج أي فقرة.

**كشف النص المكتوب بالذكاء الاصطناعي: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const [, , path, language] = process.argv;
if (!key || !path) {
  throw new Error("Usage: FILEVERITY_API_KEY=... node detect-text.mjs essay.txt [en|fr|de|es|zh]");
}

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const text = await readFile(path, "utf8");
const detection = await api("POST", "/text/detections", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ text, language }),
});

const { verdict, score, ai_fraction: share, usage, segments } = detection;
console.log(`verdict: ${verdict} (score ${score.toFixed(2)})`);
console.log(`AI-written share: ${Math.round(share * 100)}% of ${usage.characters} characters`);
console.log(`language: ${detection.language}, billed units: ${usage.billed_units}`);
const points = Array.from(text);
for (const { start, end, score: passage } of segments) {
  const excerpt = points.slice(start, Math.min(end, start + 80)).join("");
  console.log(`${start}-${end} score ${passage.toFixed(2)}: ${excerpt}`);
}
```

**كشف النص المكتوب بالذكاء الاصطناعي: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python detect_text.py essay.txt [en|fr|de|es|zh]")
sys.stdout.reconfigure(encoding="utf-8")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], encoding="utf-8") as source:
    text = source.read()
language = sys.argv[2] if len(sys.argv) > 2 else None
detection = api(
    "POST",
    "/text/detections",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"text": text, "language": language},
)

usage = detection["usage"]
share = round(detection["ai_fraction"] * 100)
print(f"verdict: {detection['verdict']} (score {detection['score']:.2f})")
print(f"AI-written share: {share}% of {usage['characters']} characters")
print(f"language: {detection['language']}, billed units: {usage['billed_units']}")
for segment in detection["segments"]:
    start, end = segment["start"], segment["end"]
    excerpt = text[start : min(end, start + 80)]
    print(f"{start}-{end} score {segment['score']:.2f}: {excerpt}")
```

**مخرجات النص النموذجي في البدء السريع**

```text
verdict: mixed (score 0.97)
AI-written share: 50% of 889 characters
language: en, billed units: 1
267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri
590-809 score 0.93: Moving forward, it is crucial to implement robust quality assurance measures and
```

يحتسب `start` و`end` نقاط ترميز Unicode، لا البايتات ولا وحدات UTF-16. وسلاسل Python تُفهرَس أصلًا بحسب نقطة الترميز؛ أما برنامج Node.js فيقسّم النص أولًا باستخدام `Array.from`. راجع [إزاحات المقاطع](https://fileverity.com/ar/docs/text-detections.md#offsets).

> **ملاحظة.** يعيد كشف النص تقديرًا إحصائيًا، وليس دليلًا على هوية كاتب النص. استخدمه مُدخلًا واحدًا ضمن مراجعة بشرية، ولا تعتمد عليه وحده في اتخاذ قرارات تمس شخصًا.

## استقبال Webhooks والتحقق من التوقيعات

يُخبر Webhook خادمك عند انتهاء الفحص، فلا تحتاج إلى الاستعلام المتكرر. وهذا المستقبِل خادم HTTP مبسّط: وحدة `http` في Node.js، وFlask في Python. يتحقق من توقيع كل عملية تسليم، ويتجاهل أي عملية تسليم عالجها من قبل، ويطبع الحدث. وهو يتبع صفحة [Webhooks](https://fileverity.com/ar/docs/webhooks.md#signature).

1. اجعل المنفذ 3000 متاحًا عبر عنوان HTTPS عام، مثلًا بواسطة نفق (tunnel) إلى جهازك. ويجب أن تستخدم عناوين نقاط النهاية بروتوكول HTTPS على المنفذ الافتراضي 443 وأن تشير إلى عنوان عام.
2. في لوحة التحكم، افتح **Webhooks**، وأضف نقطة نهاية بعنوان URL `https://your-host/webhooks/fileverity` واختر أحداثها. انسخ المفتاح السري للتوقيع، الذي يبدأ بالبادئة `whsec_`. ويظهر مرة واحدة فقط.
3. احفظ البرنامج باسم `webhook-receiver.mjs` أو `webhook_receiver.py` وشغّله مع المفتاح السري: `FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs` أو `FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py`.
4. اختر **أرسل اختبارًا** على نقطة النهاية. لا ترسل مفاتيح الاختبار أي أحداث؛ أما الإجراء **أرسل اختبارًا** فيسلّم حدثًا تجريبيًا موقَّعًا بدلًا منها.

**استقبال Webhooks: JavaScript**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const secret = process.env.FILEVERITY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const port = Number(process.env.PORT) || 3000;
const seen = new Set();

function verify(headers, body) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;
  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300)) return false;
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest();
  return signatures.split(" ").some((entry) => {
    const [version, value = ""] = entry.split(",");
    const given = Buffer.from(value, "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

function handleEvent(event) {
  const { type, data } = event;
  if (data.test) {
    console.log(`Test event ${event.id}`);
  } else if (type === "analysis.completed") {
    const { level, action } = data.summary;
    console.log(`${data.id} succeeded: level ${level}, action ${action}, ${data.report_url}`);
  } else if (type === "analysis.failed") {
    console.log(`${data.id} ${data.status}: ${data.error.code}`);
  } else if (type === "balance.low") {
    const money = (cents) => `US$${(cents / 100).toFixed(2)}`;
    console.log(`Balance ${money(data.balance)} is below your ${money(data.threshold)} alert`);
  }
}

async function receive(req, res) {
  if (req.method !== "POST" || req.url !== "/webhooks/fileverity") return res.writeHead(404).end();
  const chunks = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 1_000_000) return res.writeHead(413).end();
    chunks.push(chunk);
  }
  const body = Buffer.concat(chunks);
  if (!verify(req.headers, body)) return res.writeHead(400).end();
  const event = JSON.parse(body.toString("utf8"));
  try {
    if (!seen.has(event.id)) {
      handleEvent(event);
      seen.add(event.id);
    }
  } catch (error) {
    console.error(`Could not handle ${event.id}`, error);
    return res.writeHead(500).end();
  }
  res.writeHead(200).end();
}

createServer((req, res) => receive(req, res).catch(() => res.destroy())).listen(port, () => console.log(`Listening on port ${port}`));
```

**استقبال Webhooks: Python**

```python
import base64
import hashlib
import hmac
import json
import os
import threading
import time

from flask import Flask, request

SECRET = os.environ.get("FILEVERITY_WEBHOOK_SECRET")
if not SECRET:
    raise SystemExit("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.")
KEY = base64.b64decode(SECRET.removeprefix("whsec_"))
seen = set()
lock = threading.Lock()
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000


def verify(headers, body):
    message_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp", "")
    signatures = headers.get("webhook-signature", "")
    if not message_id or not timestamp.isdecimal() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = hmac.new(KEY, signed, hashlib.sha256).digest()
    for entry in signatures.split(" "):
        version, _, value = entry.partition(",")
        try:
            given = base64.b64decode(value, validate=True)
        except ValueError:
            continue
        if version == "v1" and hmac.compare_digest(given, expected):
            return True
    return False


def handle_event(event):
    data = event["data"]
    if data.get("test"):
        print(f"Test event {event['id']}")
    elif event["type"] == "analysis.completed":
        level, action = data["summary"]["level"], data["summary"]["action"]
        print(f"{data['id']} succeeded: level {level}, action {action}, {data['report_url']}")
    elif event["type"] == "analysis.failed":
        print(f"{data['id']} {data['status']}: {data['error']['code']}")
    elif event["type"] == "balance.low":
        balance, threshold = data["balance"] / 100, data["threshold"] / 100
        print(f"Balance US${balance:.2f} is below your US${threshold:.2f} alert")


@app.post("/webhooks/fileverity")
def receive():
    body = request.get_data()
    if not verify(request.headers, body):
        return "", 400
    event = json.loads(body)
    with lock:
        try:
            if event["id"] not in seen:
                handle_event(event)
                seen.add(event["id"])
        except Exception:
            app.logger.exception("Could not handle %s", event["id"])
            return "", 500
    return "", 200


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    print(f"Listening on port {port}")
    app.run(port=port)
```

**مخرجات المستقبِل**

```text
Listening on port 3000
Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXa
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded: level L2, action accept, https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

- **المحتوى الخام للطلب.** يغطي التوقيع البايتات نفسها التي أُرسلت. تقرأ البرامج محتوى الطلب بوصفه بايتات، وتحسب HMAC-SHA256 على `webhook-id` ثم نقطة ثم `webhook-timestamp` ثم نقطة ثم تلك البايتات، مع استخدام المفتاح السري بعد فك ترميز base64 مفتاحًا لها، ولا تحلّل JSON إلا بعد التأكد من صحة التوقيع.
- **المقارنة في زمن ثابت.** تقارن `timingSafeEqual` في Node.js و`hmac.compare_digest` في Python النتيجة بكل توقيع `v1` في الترويسة `webhook-signature`.
- **هامش تفاوت الطابع الزمني.** يُرفض أي `webhook-timestamp` يبعد عن الساعة أكثر من خمس دقائق، أو لا يكون رقمًا.
- **الاستجابة السريعة.** تحصل عملية التسليم الصحيحة على `200` بمجرد معالجة الحدث، ويحصل التوقيع الخاطئ على `400`. وإذا فشلت المعالجة يرد البرنامج بالرمز `500`، وتعيد Fileverity المحاولة لاحقًا بمعرّف الحدث نفسه. راجع [التسليم وإعادة المحاولة](https://fileverity.com/ar/docs/webhooks.md#delivery).
- **التكرارات.** يجري التسليم مرة واحدة على الأقل، وتحتفظ إعادة المحاولة بمعرّف الحدث. تتذكر البرامج المعرّفات التي عالجتها وترد بالرمز `200` دون معالجة الحدث مرتين. وفي بيئة الإنتاج، استبدل المجموعة (set) الموجودة في الذاكرة بقيد فريد على معرّف الحدث في قاعدة بياناتك، وأسند العمل إلى قائمة انتظار لتبقى الاستجابة سريعة.
- **بيانات الحدث.** يحمل الحدثان `analysis.completed` و`analysis.failed` التحليل دون `result`؛ استدعِ `GET /analyses/{id}` عندما تحتاج إلى التقرير الكامل. و`balance` و`threshold` في `balance.low` بسنتات الدولار الأمريكي.

## الخطوات التالية

- شغّل مسار استقبال كاملًا مع مراجعة بشرية في [فحص المستندات في سير عمل الاستقبال](https://fileverity.com/ar/docs/intake-workflow.md).
- ابحث عن أي حقل في [مرجع API](https://fileverity.com/ar/docs/api-reference.md)، أو حمّل وثيقة OpenAPI في أدواتك الخاصة.
- ابدأ التشغيل الفعلي بمفتاح حقيقي، وحد إنفاق شهري، وWebhook للحدث `balance.low`. راجع [الأسعار والفوترة](https://fileverity.com/ar/docs/pricing.md#alerts).
- تحتاج إلى مساعدة؟ اذكر قيمة الترويسة `x-request-id` عند [التواصل مع الدعم](https://fileverity.com/ar/contact).

