# بک‌اند کاپیلاگرام

این سند مرز نسخهٔ فعلی شبکهٔ اجتماعی و گفت‌وگوی کاپیلاگرام را مشخص می‌کند. هدف
فعلی حساب شخصی، پروفایل اجتماعی، دنبال‌کردن، پست متنی مقدماتی و گفت‌وگوی دونفره
است. فروشگاه، خدمات، بانکداری و حساب حرفه‌ای هنوز فعال نیستند، اما مدل داده طوری
است که بدون ساخت User دوم به همان حساب افزوده شوند.

## حساب و هویت

- هر شخص فقط یک رکورد `identity_users` و یک مجموعه Session دارد.
- `profileType` در حال حاضر همیشه `PERSONAL` است. مقدار `PROFESSIONAL` رزرو شده
  و در آینده فقط پس از اتصال یک موجودیت تأییدشده از Commerce یا Services فعال
  می‌شود؛ کاربر نمی‌تواند آن را مستقیم از API پروفایل تغییر دهد.
- `username` شناسهٔ عمومی و یکتای کاربر است. قواعد آن سه تا سی کاراکتر، شروع با
  حرف انگلیسی کوچک و ادامه با حرف، عدد یا زیرخط است. نام‌های سیستمی رزرو شده‌اند.
- ثبت‌نام جدید باید `username` بفرستد. برای جلوگیری از خطای انتهای فرم، اپ می‌تواند
  پیش از ثبت‌نام `GET /identity/usernames/{username}/availability` را فراخوانی کند.
- ورود اپلیکیشن با فیلد `identifier` انجام می‌شود. مقدار آن می‌تواند نام کاربری،
  شمارهٔ موبایل E.164/شمارهٔ ایران با صفر ابتدایی یا کدملی ده‌رقمی ایران باشد.
  شمارهٔ ایران بدون کد کشور باید با `09` ارسال شود تا با کدملی ده‌رقمی مبهم نباشد. کدملی خام جست‌وجو
  یا ذخیره نمی‌شود؛ همان HMAC امن ثبت‌نام برای lookup استفاده می‌شود.
- برای جلوگیری از user enumeration، تمام حالت‌های رمز اشتباه یا شناسهٔ ناموجود
  خطای عمومی `AUTH_CREDENTIALS_INVALID` می‌دهند و بررسی Argon2 ساختگی نیز انجام
  می‌شود.
- حساب‌های قدیمی اپلیکیشن که `username` ندارند باید پیش از فعال‌شدن کاپیلاگرام
  backfill شوند. کاربران پنل الزامی به username ندارند.

نمونهٔ ورود:

```json
{
  "identifier": "milad_ahmadi",
  "password": "Secure123",
  "deviceId": "android-device-id"
}
```

## مرز Social

Social مالک `social_profiles`، ارتباط Follow، پست، Like و Comment است. نام، تصویر
و اطلاعات قانونی را کپی نمی‌کند؛ اطلاعات عمومی نویسنده را از User/Profile می‌خواند.

قابلیت‌های فعلی:

- پروفایل عمومی یا خصوصی با bio، website و display name؛
- Follow فوری برای پروفایل عمومی و درخواست قابل قبول/رد برای پروفایل خصوصی؛
- فهرست follower/following و درخواست‌های در انتظار؛
- جستجوی پروفایل شخصی با نام نمایشی یا نام کاربری؛
- پست متنی با دسترسی `PUBLIC` یا `FOLLOWERS`؛
- Feed زمانی معکوس، Like و Comment/reply؛ هر خلاصهٔ پست فیلد `isLiked` را
  متناسب با کاربر جاری برمی‌گرداند تا Client وضعیت اولیهٔ دکمهٔ لایک را حدس نزند؛
- حذف نرم پست و Comment و ثبت Domain Event در Transactional Outbox.
- ویرایش فقط کپشن پست توسط مالک.
- گزارش کاربر، پست و Comment با دلیل استاندارد و وضعیت قابل پیگیری برای Moderation.

Story، Highlight، Save و Share هنوز در API عمومی فعال نشده‌اند. ثبت Report و
صفحهٔ بررسی اپراتور در پنل فعال است. در مرحلهٔ Media، فایل ابتدا در دامنهٔ Media ثبت و پردازش
می‌شود و Social فقط شناسهٔ Media آماده را به پست متصل می‌کند؛ کلید داخلی MinIO/S3
نباید از Client پذیرفته شود.

مسیرهای اصلی:

```text
GET    /social/profiles/:username
POST   /social/profiles/:username/report
GET    /social/profiles?q=:query
PUT    /social/me/profile
POST   /social/profiles/:username/follow
DELETE /social/profiles/:username/follow
GET    /social/me/follow-requests
PUT    /social/me/follow-requests/:followerId
GET    /social/profiles/:username/followers
GET    /social/profiles/:username/following
POST   /social/posts/media
GET    /social/feed
GET    /social/feed/unread-count
PUT    /social/feed/read
GET    /social/profiles/:username/posts
GET    /social/posts/:postId
GET    /social/posts/:postId/media/:mediaId
GET    /social/posts/:postId/media/:mediaId/cover
PATCH  /social/posts/:postId
DELETE /social/posts/:postId
POST   /social/posts/:postId/report
PUT    /social/posts/:postId/like
DELETE /social/posts/:postId/like
POST   /social/posts/:postId/comments
GET    /social/posts/:postId/comments
DELETE /social/comments/:commentId
POST   /social/comments/:commentId/report
GET    /panel/social/reports
PATCH  /panel/social/reports/:reportId/status
```

در `PATCH /social/posts/:postId` فقط فیلد `caption` پذیرفته می‌شود و رسانه‌های
پست قابل تعویض نیستند. حذف پست فقط برای مالک آن مجاز است. حذف Comment برای
نویسندهٔ Comment یا مالک پست مجاز است. کاربر نمی‌تواند حساب یا محتوای خودش را گزارش کند
و گزارش تکراری یک کاربر برای یک هدف به‌صورت idempotent به همان رکورد قبلی
برمی‌گردد.

گزارش‌ها با یکی از انواع `USER`، `POST` یا `COMMENT` و وضعیت اولیه `PENDING`
ذخیره می‌شوند. فقط کاربر پنل دارای مجوز `content:moderate` می‌تواند فهرست
گزارش‌ها را با نوع هدف، دلیل و وضعیت فیلتر کند یا وضعیت را به `REVIEWING`،
`RESOLVED` یا `REJECTED` تغییر دهد. هر تغییر وضعیت در Audit Log ثبت می‌شود.

هر پست الزاماً بین ۱ تا ۱۰ تصویر یا ویدئو دارد و کپشن متنی اختیاری است. ساخت
پست صرفاً متنی در API پشتیبانی نمی‌شود. هر فایل با نام فیلد تکرارشوندهٔ
`media` ارسال می‌شود. تصویر ورودی حداکثر ۱۰ MiB است. ویدئوی خام تا سقف ایمنی
۵۰۰ MiB برای هر فایل و مجموع ورودی تا ۱ GiB پذیرفته می‌شود؛ این سقف مربوط به
ingestion است و ویدئو بعد از دریافت، مستقیم برای پخش استفاده نمی‌شود.
API فایل‌های بزرگ را به‌صورت stream در فایل موقت دریافت و با stream به MinIO/S3
منتقل می‌کند؛ بنابراین کل ویدئو هم‌زمان در حافظهٔ Node.js نگهداری نمی‌شود. Client
در این مرحله درصد آپلود را از بایت‌های ارسال‌شده محاسبه می‌کند.

برای هر ویدئو وضعیت `PROCESSING` ثبت و رویداد Transactional Outbox ایجاد می‌شود.
Outbox کار را با شناسهٔ پایدار به صف BullMQ به نام `media-videos` تحویل می‌دهد.
Worker با FFmpeg خروجی MP4 شامل H.264/AAC، `faststart`، حداکثر ابعاد 1080×1920
و حداکثر ۳۰ فریم بر ثانیه تولید می‌کند و از ثانیهٔ اول ویدئو یک کاور JPEG
می‌سازد. در پایان، وضعیت به `READY` تغییر کرده و فایل خام حذف می‌شود. خطاها با
backoff در صف Retry می‌شوند و وضعیت `FAILED` برای مشاهده و پیگیری نگه داشته
می‌شود. فیلد `processingProgress` عددی بین صفر تا صد است؛ Worker زمان خروجی
FFmpeg را نسبت به مدت ویدئو محاسبه و آن را در PostgreSQL ثبت می‌کند تا Client
درصد واقعی تبدیل را با polling پست نمایش دهد. مقدار فایل آماده `100` است.

PostgreSQL متادیتا، مالکیت، وضعیت پردازش و `sortOrder` را نگه می‌دارد و فایل‌ها
در MinIO/S3 قرار دارند. endpoint دانلود قبل از تحویل هر فایل، دسترسی مشاهدهٔ
همان پست را بررسی می‌کند. ویدئوی آماده با HTTP Range و پاسخ `206` تحویل می‌شود
تا seek و پخش تدریجی در iOS و Android پایدار باشد. کلید داخلی Storage هیچ‌گاه
به Client داده نمی‌شود.

## مرز Messaging

PostgreSQL منبع حقیقت Conversation و Message است. Redis فقط هماهنگی Socket،
Presence و Typing را انجام می‌دهد. در نسخهٔ فعلی Conversation از نوع `DIRECT`
ساخته می‌شود؛ enum نوع `GROUP` برای توسعهٔ بعدی وجود دارد ولی endpoint ساخت گروه
عمداً ارائه نشده است.

- کلید مرتب‌شدهٔ دو User مانع ساخت چند گفت‌وگوی دونفرهٔ تکراری می‌شود.
- `clientMessageId` در محدودهٔ Sender یکتا است و ارسال مجدد امن را ممکن می‌کند.
- پیام‌های `TEXT` و `STICKER` پشتیبانی می‌شوند. بدنهٔ Sticker یک کلید پایدار است
  تا بسته‌های تصویری یا WebP در آینده بدون تغییر API گفت‌وگو اضافه شوند.
- پاسخ به پیام فقط وقتی مجاز است که پیام مرجع در همان Conversation باشد.
- فقط Participant می‌تواند پیام‌ها را ببیند، بفرستد یا وضعیت خواندن را تغییر دهد.
- حذف پیام نرم است تا ترتیب، Audit و سازگاری Clientها حفظ شود.
- `message.created`، `message.read` و `message.deleted` در Outbox ثبت می‌شوند.
- API پس از Commit، رویداد `message.created` را برای کمترین تأخیر به Redis Realtime
  منتشر می‌کند؛ Outbox Worker همان شناسهٔ رویداد را برای بازیابی خطا بازپخش می‌کند.
- Realtime با کلید رویداد در Redis از تحویل تکراری جلوگیری می‌کند و پیام را فقط
  به Room اختصاصی Userهای دریافت‌کننده می‌فرستد؛ Sender پاسخ قطعی را از REST می‌گیرد.
- Realtime پیش از join یا ارسال Typing، عضویت Conversation را از PostgreSQL
  بررسی می‌کند؛ دانستن UUID اتاق به‌تنهایی مجوز ورود نیست.

## کارتابل و اعلان‌های کاپیلاگرام

کارتابل مرکز اعلان پایدار کاربر است و در این مرحله لایک، دیدگاه و دنبال‌کردن را
پوشش می‌دهد. اعلان در همان Transaction عملیات Social ثبت می‌شود تا نتیجهٔ اصلی و
اعلان از هم جدا نشوند. `eventKey` یکتا از اعلان تکراری جلوگیری می‌کند، عملیات خود
کاربر برای خودش اعلان نمی‌سازد و وضعیت خوانده‌شدن در PostgreSQL ذخیره می‌شود.
برداشتن لایک، دیدگاه یا Follow اعلان متناظر را نیز حذف می‌کند. Client با لمس اعلان
به پست یا پروفایل مربوط هدایت می‌شود.

```text
GET /cartable/notifications
GET /cartable/notifications/unread-count
PUT /cartable/notifications/:notificationId/read
PUT /cartable/notifications/read-all
```

مسیرهای اصلی:

```text
POST   /messaging/conversations/direct
GET    /messaging/conversations
GET    /messaging/conversations/unread-count
GET    /messaging/conversations/:conversationId/messages
POST   /messaging/conversations/:conversationId/messages
PUT    /messaging/conversations/:conversationId/read
DELETE /messaging/messages/:messageId
```

## Badge فعالیت کاپیلاگرام

شمارندهٔ فعالیت فقط در حافظهٔ Socket نگهداری نمی‌شود. تعداد پیام‌های
خوانده‌نشده مستقیماً براساس `lastReadAt` هر Participant از PostgreSQL محاسبه
می‌شود. آخرین مشاهدهٔ Feed نیز برای هر User در `social_feed_read_states` ذخیره
می‌شود و پست‌های قابل‌مشاهدهٔ دیگران بعد از آن زمان، دیده‌نشده محسوب می‌شوند.

Badge نوار پایین مجموع پیام‌های خوانده‌نشده و پست‌های دیده‌نشده است؛ Badge دایرکت
داخل کاپیلاگرام فقط تعداد پیام‌های خوانده‌نشده را نمایش می‌دهد. رویداد Socket
شمارندهٔ پیام را بلافاصله افزایش می‌دهد و API در شروع، بازگشت به برنامه و بازه‌های
دوره‌ای آن را با دیتابیس تطبیق می‌دهد.

Redis Realtime عمداً AOF ندارد، چون Presence، Typing و هماهنگی Socket داده‌های
موقت هستند. پیام‌ها، وضعیت خواندن و Cursor مشاهدهٔ Feed در PostgreSQL باقی
می‌مانند. این تنظیم مانع توقف Gateway در اثر خطای فایل AOF می‌شود.

## توسعهٔ آینده بدون شکستن قرارداد

1. MediaAsset و پیوست‌های Post/Message با پردازش Worker اضافه می‌شوند؛ Message
   اصلی همچنان در PostgreSQL می‌ماند.
2. Outbox dispatcher رویدادهای commit‌شده را به Redis Realtime منتشر می‌کند و
   Gateway آن‌ها را فقط به Roomهای مجاز می‌فرستد.
3. Story و Highlight روی Social/Media ساخته می‌شوند و Worker با `expiresAt`
   چرخهٔ عمر نمایش ۲۴ ساعته را کنترل می‌کند.
4. Block، Report و Moderation پیش از Discovery گسترده افزوده می‌شوند.
5. فعال‌سازی Professional Profile با Event از Commerce/Services انجام می‌شود؛
   Social فقط badge و capabilityهای مجاز را نمایش می‌دهد و مالک محصول یا خدمت
   نمی‌شود.
6. Group/Channel در همان Messaging domain با Participant role و سیاست عضویت
   مستقل اضافه می‌شود.

تمام timestampها `TIMESTAMPTZ` و خروجی آن‌ها رشتهٔ ISO-8601 UTC است. تقویم شمسی
یا میلادی فقط مسئولیت Client براساس زبان انتخاب‌شده است.
