# لایه‌ی Telegram

Telegram فقط **Interface** است. هیچ قانون کسب‌وکار یا SQL در `app/Telegram` وجود ندارد؛ Handlerها ورودی را می‌خوانند، یک Service را صدا می‌زنند و نتیجه را نمایش می‌دهند.

## مسیر یک Update

```text
Telegram ──POST /webhook/{bot}──► WebhookController
    1. ربات از bot_key        2. hash_equals(secret_token)      3. update_id معتبر
    4. تکراری؟ (Redis SET NX + UNIQUE در telegram_updates)       5. push به صف      6. 200 OK
                     │
        chat_member ─┴─► صف joins   → ProcessChatMemberJob → JoinTrackingService
        بقیه        ────► صف updates → ProcessUpdateJob    → Dispatcher
```

`Dispatcher::handle()`:

```text
my_chat_member → وضعیت ربات در کانال / Block شدن توسط کاربر
فقط چت خصوصی → Context
Middleware:  ۱) AbuseGuard (نرخ، Burst، قفل موقت)
             ۲) ثبت‌نام / به‌روزرسانی پروفایل (+ ref_xxx)
             ۳) حساب مسدود
             ۴) Maintenance (فقط مدیران عبور می‌کنند)
             ۵) عضویت اجباری (با Cache)
Route:       «❌ لغو» / /cancel → پاک شدن State
             /command            → routes/telegram.php → commands
             متن دکمه‌ی منو      → buttons
             State فعال          → Flow
             callback_data       → callbacks[prefix]::callback(action, args)
خطا:         UserException → متن ترجمه‌شده | سایر → لاگ + «مشکلی پیش آمد» + کد پیگیری
```

## فایل‌ها

| فایل | نقش |
|---|---|
| `BotApi` | تنها جایی که با Bot API صحبت می‌کند (Adapter). خطاها ← `TelegramException` (`isFloodWait`, `isUnreachableUser`, …) |
| `Update` | دسترسی تایپ‌شده به Update خام |
| `Context` | `user`, `admin`, `t()`, `reply()`, `say()`, `show()`, `answer()`, `state()`, `home()` |
| `Dispatcher` | Middleware + مسیریابی + تبدیل Exception به پیام |
| `StateManager` / `State` | وضعیت گفتگو در Redis با TTL (`app.state_ttl`، پیش‌فرض ۳۰ دقیقه) |
| `Keyboards` / `Ui` | ساخت Keyboard از `resources/keyboards`، دکمه‌های Inline، صفحه‌بندی، نوار پیشرفت. `Ui::label()` رنگ (`!primary`) و آیکون (`{i:name}`) را از متن دکمه جدا می‌کند |
| `Core\I18n\IconSet` | تبدیل `{i:name}` به ایموجی سفارشی (`resources/icons.php`) یا ایموجی معمولی |
| `ForceJoin` | بررسی عضویت با Cache (عضو: ۱۰ دقیقه، غیرعضو: ۲۰ ثانیه) |
| `Flows/*` | فرآیندهای چندمرحله‌ای |
| `Handlers/*` | صفحه‌های کاربر؛ `Handlers/Admin/*` پنل مدیریت |

## Flow (State Machine)

هر Flow کلاس `Flow` را ارث می‌برد و سه متد دارد: `first()`، `prompt()`، `accept()`.

| Flow | مراحل |
|---|---|
| `order` | نوع → کانال (اگر فقط یک کانال باشد پرسیده نمی‌شود) → تعداد → قیمت → تأیید. تعداد و قیمت با یک لمس انتخاب می‌شوند یا تایپ می‌شوند؛ عنوان همان نام کانال است |
| `deposit` | مبلغ (دکمه‌های آماده یا تایپ) → درگاه |
| `withdraw` | روش → مبلغ (دکمه‌ی «کل موجودی») → مقصد (حساب‌های ذخیره‌شده یا تایپ) → تأیید |
| `transfer` | recipient → amount → confirm |
| `bank` | holder → card |
| `kyc` | name → national_id → phone → document → confirm |
| `ticket` | subject → body |
| `broadcast` | target → content → buttons → confirm |
| `input` | یک پرسش عمومی (هش تراکنش، دلیل رد، جستجو، مقدار تنظیم و …) |

رفتار مشترک (در کلاس پایه):

- **❌ لغو** همیشه و در هر مرحله State را پاک می‌کند (در Dispatcher، قبل از هر چیز).
- **🔙 بازگشت** به مرحله‌ی قبل برمی‌گردد (پشته‌ی `history`).
- ورودی نامعتبر (`ValidationException`) همان مرحله را دوباره می‌پرسد.
- زدن دکمه‌ی منوی اصلی، Flow نیمه‌تمام را رها می‌کند.
- State بعد از TTL خودبه‌خود منقضی می‌شود؛ کاربر هیچ‌وقت در State گیر نمی‌کند.
- در مرحله‌ی تأیید یک کلید Idempotency ساخته می‌شود (`requestKey`)؛ دو بار زدن «✅ تأیید» یک عملیات می‌سازد.
- داده‌ی حساس (مقصد برداشت، کد ملی، موبایل) در State به‌صورت رمزشده نگه‌داری می‌شود.
- عملیات طولانی: «⏳ در حال پردازش...» ارسال و سپس با نتیجه جایگزین می‌شود (`Context::replace`).

## منوها

ناوبری با Reply Keyboard است (مثل ربات قبلی) و جزئیات، فهرست‌ها و عملیات با دکمه‌های شیشه‌ای:

| منو | فایل | دکمه‌ها |
|---|---|---|
| اصلی | `keyboards/main.php` | تبلیغ‌های فعال، سفارش تبلیغ، لینک‌های من، سفارش‌های من، گزارش درآمد، امور مالی، زیرمجموعه‌گیری، حساب کاربری، پشتیبانی، راهنما (+ پنل مدیریت برای مدیران) |
| امور مالی | `keyboards/finance.php` | موجودی من، شارژ، برداشت، انتقال، تراکنش‌ها، سوابق برداشت، حساب‌های بانکی، کیف‌های رمزارز، بازگشت |
| حساب کاربری | `keyboards/account.php` | کانال‌های من، احراز هویت، رتبه‌بندی، تنظیمات، بازگشت |
| پنل مدیریت | `keyboards/admin.php` | پنل کاربر، داشبورد، گزارش‌ها، برداشت‌ها، کمپین‌ها، کاربران، جستجو، سفارش‌ها، پرداخت‌ها، تیکت‌ها، درخواست‌های احراز، پیام همگانی، رفتار مشکوک، تنظیمات سیستم، مدیران، سایر ابزارها |

- دکمه‌ی فشرده‌شده از روی **کلمات** برچسب شناخته می‌شود (`Ui::plain`)، مستقل از آیکون و رنگ؛ پس خاموش/روشن کردن آیکون‌های پرمیوم یا منوی قدیمیِ مانده روی گوشی کاربر مشکلی ایجاد نمی‌کند.
- در `routes/telegram.php` هر دکمه یا به `[Handler, method]` می‌رود یا با `[Handler, 'callback', action, args]` مستقیماً یک صفحه‌ی شیشه‌ای را باز می‌کند.
- «بازگشت» داخل Flow یک مرحله عقب می‌رود و بیرون از Flow به منوی اصلی برمی‌گردد.
- مدیری که وارد پنل شده تا زدن «پنل کاربر» در منوی مدیریت می‌ماند (`Context::inAdminMode`)؛ پایان هر عملیات، همان منو را برمی‌گرداند.
- «تبلیغ‌های فعال» هر تبلیغ را به‌صورت یک **کارت** جدا (درآمد هر عضو، نوار پیشرفت، ظرفیت باقی‌مانده) با دکمه‌ی «دریافت لینک اختصاصی» نشان می‌دهد.

## قالب `callback_data`

`prefix:action:arg1:arg2` (حداکثر ۶۴ بایت). پیشوندها در `routes/telegram.php`:

| پیشوند | Handler | پیشوند | Handler |
|---|---|---|---|
| `acc` | حساب و کانال‌ها | `adm` | پنل: منو، داشبورد، گزارش، لاگ |
| `wal` | کیف پول و پرداخت | `au` | پنل: کاربران |
| `ads` | تبلیغ‌ها و لینک‌ها | `af` | پنل: مالی |
| `ord` | سفارش‌ها | `ac` | پنل: کمپین، کانال، ریسک، تعرفه |
| `fin` | امور مالی | `as` | پنل: تیکت، احراز هویت، همگانی |
| `ref`, `sup`, `set` | زیرمجموعه، پشتیبانی، تنظیمات | `ay` | پنل: تنظیمات و مدیران |
| `flow:<value>` | پاسخ Inline به مرحله‌ی جاری Flow | `fj:check` | بررسی عضویت اجباری |

هیچ Handlerی به `callback_data` اعتماد نمی‌کند: مالکیت (`forUser`, `ownedBy`) و مجوز (`Authorizer`) در Service بررسی می‌شود. دکمه‌ی جعلی فقط «دسترسی ندارید» می‌گیرد.

## افزودن یک صفحه‌ی جدید

1. متن‌ها را به `resources/messages/fa.php` اضافه کنید.
2. Handler بسازید (سازنده = وابستگی‌ها؛ Container خودش تزریق می‌کند):

```php
final class CouponHandler
{
    public function __construct(private readonly CouponService $coupons) {}

    public function show(Context $c): void
    {
        $c->show($c->t('coupon.menu'), Ui::inline([Ui::btn($c->t('coupon.btn_list'), 'cpn:list:1')]));
    }

    public function callback(Context $c, string $action, array $args): void { /* … */ }
}
```

3. در `routes/telegram.php` ثبت کنید: `buttons` (کلید دکمه) و `callbacks` (پیشوند).
4. برای دکمه‌ی منوی اصلی، کلید را به `resources/keyboards/main.php` اضافه کنید.

`tests/Unit/MessagesTest` بررسی می‌کند که هر کلید استفاده‌شده در کد، در فایل متن‌ها وجود داشته باشد.

## محدودیت نرخ تلگرام

- پاسخ‌های تعاملی مستقیم ارسال می‌شوند.
- پیام‌های خودکار (اعلان، همگانی) از `RateLimiter::throttle("tg:{bot}")` با سقف `TELEGRAM_RATE_PER_SECOND` می‌گذرند.
- خطای `429`: Job با `RetryLater(retry_after)` به صف برمی‌گردد **بدون** مصرف Attempt؛ Worker کرش نمی‌کند و چیزی گم نمی‌شود.
- خطای `403` (کاربر ربات را Block کرده): `bot_users.is_blocked = 1` و دیگر در Broadcast هدف قرار نمی‌گیرد.
