این سند رفتار فعلی سامانه را بر اساس کد موجود توضیح میدهد و برای تبدیل مستقیم به فلوچارت، BPMN، Sequence Diagram یا Swimlane Diagram نوشته شده است. هر جا بین رفتار مطلوب و رفتار فعلی تفاوتی وجود داشته باشد، عبارت «در پیادهسازی فعلی» استفاده شده است.
هدف سامانه این است که یک کاربر مجاز در گروه فعال تلگرام، برای یکی از شمارههای ثبتشده درخواست کد تأیید دیوار ایجاد کند؛ گوشی دریافتکننده SMS کد را استخراج و به بکاند ارسال کند؛ و بکاند کد را فقط در چت خصوصی درخواستکننده تحویل دهد. کد نباید در گروه تلگرام نمایش داده شود.
این سند پنج حوزه را پوشش میدهد:
- آمادهسازی و راهاندازی بکاند و ربات تلگرام؛
- نصب و آمادهسازی اپ اندروید؛
- ثبت شمارهها و درخواست کد توسط کاربر؛
- دریافت SMS، صف محلی، ارسال، تطبیق نشست و تحویل کد؛
- همه حالتها، شرطها، خطاها، انقضاها، retryها و دادههای ذخیرهشده.
flowchart LR
U[کاربر درخواستکننده] -->|دستور code در گروه| TG[گروه تلگرام فعال]
TG -->|Webhook update| BE[بکاند Express]
BE -->|پیام خصوصی آمادگی نشست| U
U -->|ورود شماره در دیوار| DV[دیوار]
DV -->|SMS کد تأیید| PH[گوشی دارای اپ Receiver]
PH -->|HTTPS: POST api/sms| BE
BE -->|تطبیق شماره با نشست فعال| SS[(Session Store)]
BE -->|کد فقط در چت خصوصی| U
BE -->|تأیید بدون کد| TG
BE --> PS[(Phone Store)]
PH --> LQ[(صف و لاگ محلی)]
- اپ اندروید مستقیماً به Telegram API وصل نمیشود و Bot Token داخل اپ نیست.
- اپ فقط کد ششرقمی و یک یا دو شماره کاندید را برای بکاند میفرستد؛ متن کامل SMS و فرستنده به بکاند ارسال نمیشوند.
- کد در گروه منتشر نمیشود؛ فقط پیام موفقیت یا انقضا در گروه ثبت میشود.
- بکاند تصمیم میگیرد کد متعلق به کدام نشست است؛ تشخیص اسلات سیمکارت در اندروید فقط ترتیب کاندیدها را تغییر میدهد.
برای فلوچارت نهایی، Laneها با این ترتیب پیشنهاد میشوند:
| شناسه Lane | بازیگر/جزء | مسئولیت |
|---|---|---|
L1 |
مدیر سامانه | تنظیم env، استقرار بکاند، ثبت webhook، نصب APK و آمادهسازی گوشی |
L2 |
کاربر درخواستکننده | Start کردن ربات، انتخاب شماره، ارسال /code، ورود شماره در دیوار و دریافت کد خصوصی |
L3 |
گروه/پلتفرم تلگرام | تحویل فرمانها به webhook و تحویل پیامهای ربات به گروه یا چت خصوصی |
L4 |
بکاند Express | اعتبارسنجی، مدیریت نشست، تطبیق SMS، audit و ارسال پیام تلگرام |
L5 |
Phone Store | ثبت شمارهها و نگهداری شمارندههای استفاده/موفقیت |
L6 |
Session Store | نگهداری نشستهای فعال، انقضا و تاریخچه audit |
L7 |
سرویس دیوار/شبکه اپراتور | تولید و ارسال SMS کد تأیید |
L8 |
Android OS | Broadcast دریافت SMS، مجوزها، Boot event، JobScheduler و محدودیت باتری |
L9 |
اپ Android Receiver | تنظیمات، تشخیص SMS، استخراج کد، صف محلی و ارسال به بکاند |
L10 |
حافظه محلی اپ | SharedPreferences تنظیمات، صف pending، fingerprintهای dedup و لاگ محلی |
در agent_preferences ذخیره میشوند:
enabled: وضعیت ON/OFF عامل دریافت؛ مقدار اولیهfalseاست.sim_1_phone: شماره اول؛ نام فیلد تاریخی است و الزاماً به اسلات واقعی SIM 1 وابسته نیست.sim_2_phone: شماره دوم و اختیاری.autostart_confirmed: تأیید دستی کاربر برای فعال بودن Autostart در خانواده Xiaomi.
شماره معتبر دقیقاً باید با الگوی 09xxxxxxxxx، یعنی ۱۱ رقم و شروع با 09، تطبیق داشته باشد. حداقل یک و حداکثر دو شماره در API پذیرفته میشود. شمارههای تکراری در خروجی تنظیمات محلی حذف میشوند.
هر آیتم PendingDelivery شامل این فیلدهاست:
id: هش SHA-256 ازsender + body + مجموعه مرتبشده شمارهها؛code: کد نرمالشده ششرقمی؛phones: یک یا دو شماره کاندید؛createdAt: زمان دریافت SMS بر حسب millisecond epoch.
صف به صورت FIFO خوانده میشود و قبل از پایان Broadcast به شکل synchronous روی دیسک commit میشود. fingerprintهای دیدهشده ۲۴ ساعت نگهداری میشوند.
فایل فعال بر اساس mode انتخاب میشود:
dev: فایلphones.json؛product: فایلphones.product.json.
هر شماره شامل موارد زیر است:
timesCalled: تعداد درخواستهای معتبر/codeبرای شماره ثبتشده؛successfulDeliveries: تعداد تحویلهای خصوصی موفق؛addedAt: زمان اولین ثبت؛updatedAt: زمان آخرین تغییر شمارنده.
ثبت مجدد شماره، رکورد را duplicate نمیکند و شمارندهها را reset نمیکند.
فایل فعال بر اساس mode انتخاب میشود:
dev: فایلsessions.json؛product: فایلsessions.product.json.
کلید هر نشست فعال، خود شماره تلفن است. داده نشست:
sessionId: UUID؛state: در رکورد فعال برابرactive؛requesterId: شناسه Telegram کاربر؛requesterDisplayName: username یا نام نمایشی؛groupChatId: گروهی که فرمان در آن ثبت شده؛requestMessageId: پیام اصلی/codeبرای reply کردن؛createdAt: زمان ایجاد؛expiresAt: زمان ایجاد + ۱۲۰ ثانیه.
کلید ویژه _history آرایه append-only رخدادهاست. نوشتن فایل با فایل موقت و rename انجام میشود و عملیات storeها serial است تا race condition درخواستهای همزمان کنترل شود.
- بکاند فقط یکی از modeهای
devیاproductرا فعال میکند. TELEGRAM_DEV_GROUP_IDوTELEGRAM_PRODUCT_GROUP_IDهر دو باید عدد منفی و متفاوت باشند.- تنها گروه متناظر با mode، «گروه فعال» است.
- تمام فرمانهای عملیاتی به جز
/startو/group_idدر گروه غیر فعال نادیده گرفته میشوند. - اپ در هر درخواست
X-App-Modeمیفرستد؛ اگر با mode بکاند برابر نباشد، پاسخ409 app_mode_mismatchدریافت میکند. - داده نشست و شماره dev و product در فایلهای جدا ذخیره میشود.
- برای تغییر محیط باید env بکاند تغییر کند، بکاند restart شود و APK با mode متناظر rebuild/reinstall شود.
- در پیادهسازی فعلی URL بکاند در
app/build.gradle.ktsرویhttps://mlk-dvr-receiver.darkube.irثابت شده است؛ Secret و Mode از Gradle property، environment یاbackend/.envوارد BuildConfig میشوند.
| ID | Lane | عمل/تصمیم | مسیر بعدی |
|---|---|---|---|
BE-BOOT-01 |
مدیر | اجرای npm start / node src/server.js |
BE-BOOT-02 |
BE-BOOT-02 |
بکاند | بارگذاری env | BE-BOOT-03 |
BE-BOOT-03 |
بکاند | آیا همه متغیرهای ضروری موجودند؟ | بله: 04؛ خیر: log و exit با کد ۱ |
BE-BOOT-04 |
بکاند | آیا APP_MODE یکی از dev/product است؟ |
بله: 05؛ خیر: exit |
BE-BOOT-05 |
بکاند | آیا هر دو Group ID منفی و متفاوتاند؟ | بله: 06؛ خیر: exit |
BE-BOOT-06 |
بکاند | تعیین DATA_DIR و فایلهای mode |
07 |
BE-BOOT-07 |
Session/Phone Store | ساخت پوشه و خواندن JSON | معتبر: 08؛ فایل مفقود/خالی/JSON خراب: بازسازی {} سپس 08 |
BE-BOOT-08 |
بکاند | حذف نشستهای منقضی و migration نشست legacy در transaction اولیه | 09 |
BE-BOOT-09 |
بکاند | برای هر نشست active باقیمانده timer انقضا را restore میکند | 10 |
BE-BOOT-10 |
بکاند | اعتبارسنجی PORT بین ۱ تا ۶۵۵۳۵ |
معتبر: 11؛ نامعتبر: exit |
BE-BOOT-11 |
بکاند | listen روی 0.0.0.0:PORT |
سرویس Ready |
- در
SIGTERMیاSIGINTفقط اولین درخواست shutdown پذیرفته میشود. - سرور پذیرش اتصال جدید را متوقف میکند.
- timerهای نشست پاک و expiration handler جدا میشود.
- اگر shutdown تا ۱۰ ثانیه کامل نشود، اتصالهای باقیمانده force-close و process با خطا خاتمه مییابد.
- مدیر باید webhook تلگرام را روی
POST /telegram/webhookثبت کند؛ بکاند آن را خودکار ثبت نمیکند. GET /healthبدون احراز هویت،200 {"status":"ok"}برمیگرداند.- Telegram Client برای
sendMessageاز HTML parse mode استفاده میکند و timeout آن ۲۰ ثانیه است.
- اپ
MainActivityرا باز میکند. - تنظیمات محلی قبلی خوانده میشوند؛ کلیدهای legacy مربوط به URL و Secret محلی حذف میشوند.
- شمارههای ذخیرهشده و وضعیت ON/OFF در UI نمایش داده میشوند.
- readiness محاسبه میشود:
- مجوز
RECEIVE_SMS؛ - مجوز
READ_SMSبرای recovery؛ - خارج بودن از Battery Optimization؛
- روی Xiaomi/Redmi/Poco، تأیید Autostart.
- مجوز
- در build نوع dev، برچسب
Mode: DEVنمایش داده میشود.
| ID | Lane | عمل/تصمیم | نتیجه |
|---|---|---|---|
APP-CONFIG-01 |
مدیر گوشی | وارد کردن شماره اول و در صورت نیاز شماره دوم | 02 |
APP-CONFIG-02 |
اپ | حذف فاصله ابتدا/انتها | 03 |
APP-CONFIG-03 |
اپ | آیا حداقل یک شماره وارد شده و همه شمارههای واردشده معتبرند؟ | بله: ذخیره هر دو فیلد؛ خیر: عدم ذخیره مقادیر جدید |
APP-CONFIG-04 |
اپ | نمایش Toast | موفق: «تنظیمات ذخیره شد»؛ ناموفق: درخواست شماره معتبر |
ترتیب شمارهها برای تطبیق نهایی مهم نیست؛ با این حال metadata اسلات میتواند یکی را در ابتدای آرایه کاندید قرار دهد.
- کاربر Switch را ON میکند.
- اپ ابتدا ورودیهای فعلی شمارهها را اعتبارسنجی و ذخیره میکند.
- اگر نامعتبر باشند، Switch به OFF برمیگردد و فرایند متوقف میشود.
- اگر هر یک از مجوزهای
RECEIVE_SMSیاREAD_SMSموجود نباشد، هر دو مجوز مفقود درخواست میشوند. - اگر
RECEIVE_SMSرد شود:enabled=false؛- Job لغو؛
- Switch خاموش؛
- Toast خطا.
- اگر
RECEIVE_SMSداده شود ولیREAD_SMSداده نشود، عامل میتواند روشن شود اما recovery پیامهای ازدسترفته غیرفعال است و هشدار نمایش داده میشود. - اپ
enabled=trueمیکند و JobScheduler را با شرایط زیر ثبت میکند:- نیاز به هر نوع شبکه؛
- persisted پس از reboot؛
- backoff نمایی با پایه ۳۰ ثانیه.
- اگر Android ثبت Job را رد کند یا exception رخ دهد، اپ دوباره OFF میشود.
- در حالت موفق، log «Agent switched ON» ثبت و recovery فوری اجرا میشود.
- محدودیت باتری یا Autostart ناقص مانع ON شدن نیست؛ فقط هشدار reliability داده میشود.
enabled=falseذخیره میشود.- Job با ID ثابت
3001لغو میشود. - log محلی ثبت میشود.
- Broadcastهای بعدی SMS بدون پردازش return میشوند.
- آیتمهای موجود صف پاک نمیشوند، اما تا روشن شدن دوباره ارسال نخواهند شد و احتمالاً پس از ۶۰ ثانیه منقضی میشوند.
- اگر عامل روشن باشد ولی
RECEIVE_SMSrevoke شده باشد، اپ خودکار OFF و Job لغو میشود. - readiness دوباره محاسبه میشود.
- پس از بازگشت از صفحه Xiaomi Autostart، کاربر با Dialog تأیید میکند که Autostart را فعال کرده یا نه.
- اگر عامل روشن و
RECEIVE_SMSموجود ولیREAD_SMSمفقود باشد، در هر launch فقط یک بار مجوز recovery درخواست میشود. - recovery پیامهای اخیر اجرا میشود.
- آخرین logهای محلی نمایش داده میشوند.
| ID | Lane | عمل/تصمیم | مسیر/نتیجه |
|---|---|---|---|
TEST-01 |
مدیر گوشی | لمس TEST AND SEND PHONE TO GROUP |
02 |
TEST-02 |
اپ | اعتبارسنجی و ذخیره ۱ یا ۲ شماره | نامعتبر: Toast و پایان؛ معتبر: 03 |
TEST-03 |
اپ | غیرفعال کردن موقت دکمه و ارسال HTTPS POST /api/test |
04 |
TEST-04 |
بکاند | بررسی X-App-Secret با مقایسه timing-safe |
نامعتبر: HTTP 401؛ معتبر: 05 |
TEST-05 |
بکاند | بررسی X-App-Mode |
mismatch: HTTP 409؛ معتبر: 06 |
TEST-06 |
بکاند | بررسی body شامل ۱ تا ۲ شماره معتبر | نامعتبر: HTTP 400؛ معتبر: 07 |
TEST-07 |
بکاند/تلگرام | ارسال Receiver Test Message به گروه فعال | شکست: audit + HTTP 502؛ موفق: 08 |
TEST-08 |
Phone Store | ثبت شمارههای جدید بدون reset شمارنده قبلی | خطای storage: audit + HTTP 500؛ موفق: 09 |
TEST-09 |
Session Store | audit با state=sent_to_group |
10 |
TEST-10 |
بکاند | پاسخ 200 {success:true} |
11 |
TEST-11 |
اپ | log نتیجه، فعالکردن دکمه و نمایش Toast | پایان |
نکته: انجام TEST برای روشن شدن عامل الزامی نیست، اما برای اینکه /code آن شماره پذیرفته شود، شماره باید قبلاً در Phone Store ثبت شده باشد.
در گروه فعال، هر پیام متنی که دقیقاً این ساختار را داشته باشد نیز شمارهها را ثبت میکند:
📱 Receiver Test Message
شمارههای تلفن:
09901283916
#phone
یک یا دو شماره پذیرفته میشود. در پیادهسازی فعلی بکاند بررسی نمیکند که فرستنده این پیام خود ربات بوده باشد؛ هر عضو گروه فعال با قالب دقیق میتواند شماره ثبت کند.
- کاربر باید حداقل یک بار چت خصوصی ربات را باز و Start کند.
- Telegram update به webhook میرسد.
- اگر متن دقیقاً
/startیا/start@BotUsernameباشد، ربات در همان chat باسلام!پاسخ میدهد. - رخداد با نوع
bot_commandو staterepliedیاtelegram_failedaudit میشود. - این فرمان هم در private و هم group پاسخ داده میشود.
اهمیت این مرحله: Telegram اجازه نمیدهد Bot پیش از شروع گفتوگوی خصوصی توسط کاربر، پیام خصوصی آغاز کند. اگر این مرحله انجام نشده باشد، ایجاد نشست کد در مرحله ارسال پیام خصوصی شکست میخورد.
فرمان معتبر:
/code 09xxxxxxxxx
فرمان با suffix ربات و حروف بزرگ/کوچک نیز پذیرفته میشود؛ مانند /Code@BotUsername 09901283916.
| ID | Lane | عمل/تصمیم | مسیر/نتیجه |
|---|---|---|---|
CODE-REQ-01 |
کاربر | ارسال /code PHONE |
02 |
CODE-REQ-02 |
تلگرام | POST update به /telegram/webhook |
03 |
CODE-REQ-03 |
بکاند | آیا update دارای message، chat.id و text است؟ | خیر: 200 ignored؛ بله: 04 |
CODE-REQ-04 |
بکاند | آیا chat نوع group/supergroup است؟ | خیر: 200 ignored؛ بله: 05 |
CODE-REQ-05 |
بکاند | آیا group همان گروه فعال mode است؟ | خیر: 200 ignored؛ بله: 06 |
CODE-REQ-06 |
بکاند | آیا متن شبیه /code است؟ |
خیر: 200 ignored؛ بله: 07 |
CODE-REQ-07 |
بکاند | parse دقیق فرمان و شماره | نامعتبر: audit invalid_format + reply قالب صحیح + پایان؛ معتبر: 08 |
CODE-REQ-08 |
بکاند | آیا message.from.id موجود است؟ |
خیر: ignored؛ بله: 09 |
CODE-REQ-09 |
Phone Store | incrementCallIfRegistered(phone) |
شماره موجود: افزایش timesCalled و 10؛ موجود نیست: 11A؛ خطای store: 11B |
CODE-REQ-10 |
Session Store | audit phone_scoreboard/call_incremented |
12 |
CODE-REQ-11A |
بکاند | reply «شماره در فهرست نیست» + audit phone_not_registered |
200 accepted:false |
CODE-REQ-11B |
بکاند | reply «امکان بررسی نیست» + audit phone_registry_check_failed |
200 accepted:false |
CODE-REQ-12 |
Session Store | آیا برای این شماره نشست فعال وجود دارد؟ | بله: 13A؛ خیر: 13B |
CODE-REQ-13A |
بکاند | audit duplicate_active_session و reply هشدار |
200 accepted:false؛ نشست قبلی بدون تغییر |
CODE-REQ-13B |
بکاند/تلگرام | ارسال پیام خصوصی «نشست آماده است» به requester | شکست: 14A؛ موفق: 14B |
CODE-REQ-14A |
بکاند | audit private_message_failed و reply در گروه که Start را بزند |
200 accepted:false؛ نشست ساخته نمیشود |
CODE-REQ-14B |
Session Store | ساخت نشست active با انقضای ۱۲۰ ثانیه و audit |
15 |
CODE-REQ-15 |
بکاند | ثبت timer انقضا برای phone/sessionId | 16 |
CODE-REQ-16 |
بکاند/تلگرام | reply گروه شامل شماره، درخواستکننده و مهلت؛ بدون کد | 200 accepted:true |
در پیادهسازی فعلی شمارنده قبل از بررسی duplicate بودن نشست و قبل از ارسال پیام خصوصی افزایش مییابد. بنابراین هر فرمان با قالب معتبر برای شماره ثبتشده، حتی اگر به دلیل نشست تکراری یا ناتوانی ربات در private message پذیرفته نشود، timesCalled را یک واحد زیاد میکند.
Session Store عملیات را serial میکند. اگر دو درخواست همزمان برای یک شماره برسند، فقط یکی نشست ایجاد میکند و دیگری duplicate_active_session میشود.
- پس از دریافت پیام خصوصی آمادگی نشست، کاربر شماره نمایشدادهشده را در دیوار وارد میکند.
- دیوار کد تأیید را از طریق اپراتور برای همان شماره ارسال میکند.
- Android پیام
SMS_RECEIVEDرا بهSmsReceiverمیدهد؛ این receiver در Manifest با permissionBROADCAST_SMSثبت شده است.
| ID | Lane | عمل/تصمیم | مسیر/نتیجه |
|---|---|---|---|
SMS-LIVE-01 |
Android | دریافت Broadcast | 02 |
SMS-LIVE-02 |
اپ | آیا action دقیقاً SMS_RECEIVED است؟ |
خیر: پایان؛ بله: 03 |
SMS-LIVE-03 |
اپ | آیا enabled=true است؟ |
خیر: پایان بدون log؛ بله: 04 |
SMS-LIVE-04 |
اپ | استخراج partهای SMS از Intent | خالی: پایان؛ موجود: 05 |
SMS-LIVE-05 |
اپ | اتصال body همه partها و خواندن sender از part اول | 06 |
SMS-LIVE-06 |
MessageParser | آیا body شامل واژه «دیوار» است و یک عدد مستقل ششرقمی دارد؟ | خیر: پایان؛ بله: 07 |
SMS-LIVE-07 |
MessageParser | نرمالسازی ارقام لاتین/فارسی/عربی به لاتین | 08 |
SMS-LIVE-08 |
اپ | آیا تنظیم شمارهها معتبر است؟ | خیر: log و پایان؛ بله: 09 |
SMS-LIVE-09 |
resolver | تلاش برای تشخیص slot/subscription از extraهای رسمی و vendor | 10 |
SMS-LIVE-10 |
resolver | ساخت فهرست همه شمارههای configured؛ شماره slot تشخیصدادهشده فقط اول میآید | خالی: log و پایان؛ غیرخالی: 11 |
SMS-LIVE-11 |
صف محلی | ساخت fingerprint و پاکسازی seenهای قدیمیتر از ۲۴ ساعت | 12 |
SMS-LIVE-12 |
صف محلی | آیا fingerprint قبلاً دیده شده است؟ | بله: log duplicate و پایان؛ خیر: 13 |
SMS-LIVE-13 |
صف محلی | persist آیتم pending و seen fingerprint با commit | 14 |
SMS-LIVE-14 |
اپ | log «queued verification code» | 15 |
SMS-LIVE-15 |
Android JobScheduler | schedule job نیازمند شبکه | موفق یا ناموفق log میشود؛ پایان Broadcast |
اگر هر exception در این مسیر رخ دهد، اپ آن را در LocalLog ثبت میکند و Broadcast را بدون crash کردن ادامه نمیدهد.
Recovery فقط برای زمانی است که Broadcast receiver به دلیل محدودیت OEM، توقف اپ یا شرایط مشابه اجرا نشده، ولی SMS هنوز در Inbox است.
onResumeاپ؛- بلافاصله پس از روشنشدن موفق عامل؛
- پس از grant شدن
READ_SMS؛ - پس از reboot دستگاه؛
- پس از
MY_PACKAGE_REPLACEDیعنی update اپ.
- عامل ON باشد؛
- تنظیم شماره معتبر باشد؛
READ_SMSداده شده باشد؛- در MainActivity همزمان recovery دیگری در حال اجرا نباشد.
- Inbox برای پیامهایی با
DATE >= now - 60 secondsquery میشود؛ ترتیب قدیمی به جدید است. - ابتدا projection دارای
sub_idامتحان میشود؛ اگر provider آن را نپذیرد، query بدون آن تکرار میشود. - هر پیام فقط وقتی قابل recovery است که زمان آن مثبت، نه در آینده و هنوز کمتر از ۶۰ ثانیه سن داشته باشد.
- همان MessageParser مسیر زنده روی body اعمال میشود.
- subscription ID در صورت وجود به slot تبدیل میشود؛ با این حال تمام شمارههای configured به عنوان کاندید ارسال میشوند.
- همان fingerprint مسیر زنده استفاده میشود؛ بنابراین SMS قبلاً پردازششده دوباره enqueue نمیشود.
- اگر حداقل یک پیام جدید recovery شود، JobScheduler فعال میشود.
- نبود مجوز، query ناموجود، SecurityException یا خطای عمومی با
unavailable=trueو log مناسب خاتمه مییابد.
stateDiagram-v2
[*] --> Pending: enqueue و persist
Pending --> ExpiredDiscarded: سن حداقل ۶۰ ثانیه
Pending --> Paused: Agent OFF یا تنظیم/شماره نامعتبر
Paused --> Pending: Agent دوباره ON و هنوز منقضی نشده
Pending --> RetryWaiting: خطای شبکه، HTTP غیرموفق یا success=false
RetryWaiting --> Pending: اجرای مجدد Job با backoff
Pending --> Removed: پاسخ 2xx با success=true
Removed --> [*]
ExpiredDiscarded --> [*]
- Job فقط وقتی شروع به کار میکند که
preferences.enabled=trueباشد؛ در غیر این صورت پایان مییابد. - یک generation ID برای جلوگیری از ادامه اجرای قبلی ساخته میشود.
- اگر تنظیم شماره نامعتبر باشد، ارسال pause و log میشود؛ retry صریح درخواست نمیشود.
- اولین آیتم صف خوانده میشود.
- اگر سن آیتم حداقل ۶۰ ثانیه باشد:
- آیتم حذف میشود؛
- log انقضا ثبت میشود؛
- حلقه سراغ آیتم بعدی میرود.
- شمارههای معتبر ذخیرهشده داخل خود آیتم انتخاب میشوند؛ اگر خالی باشند، شمارههای تنظیمات فعلی fallback هستند.
- اگر هیچ شمارهای موجود نباشد، ارسال pause میشود.
- اپ درخواست
POST /api/smsرا با این اطلاعات میفرستد:- Header:
Content-Type: application/json؛ - Header:
X-App-Secret؛ - Header:
X-App-Mode؛ - Body:
{"code":"123456","phones":["09..."]}.
- Header:
- timeout اتصال HTTP برابر ۱۵ ثانیه و timeout خواندن ۲۰ ثانیه است؛ redirect خودکار پذیرفته نمیشود.
- فقط پاسخ HTTP 2xx که JSON آن
success=trueباشد موفق محسوب میشود. - در موفقیت:
- آیتم صف حذف میشود؛
- log «Submitted» ثبت میشود؛
- حلقه آیتم بعدی را بررسی میکند.
- در خطای HTTP، network، timeout، JSON نامعتبر یا
success=false:- آیتم در صف باقی میماند؛
retryNeeded=true؛- حلقه متوقف میشود؛
- JobScheduler با backoff دوباره تلاش میکند، مشروط به اینکه آیتم هنوز منقضی نشده باشد.
- اگر Android اجرای Job را متوقف کند، generation تغییر میکند و تا وقتی عامل ON است،
onStopJob=trueیعنی reschedule درخواست میشود.
پاسخهای no_active_session و ambiguous_active_sessions از بکاند HTTP 200 و success=true دارند. بنابراین اپ آنها را «پذیرفتهشده توسط بکاند» میداند و آیتم را حذف میکند، حتی اگر delivered=false باشد. تنها شکست تحویل خصوصی Telegram با HTTP 502 باعث باقی ماندن آیتم و retry اپ میشود.
| شرط | پاسخ | اثر جانبی |
|---|---|---|
| Secret اشتباه/غایب | 401 {success:false,error:"unauthorized"} |
بدون audit و بدون Telegram |
| Mode اشتباه/غایب | 409 {success:false,error:"app_mode_mismatch"} |
بدون audit و بدون Telegram |
| code غیر ششرقمی یا phones خارج از ۱..۲/نامعتبر | 400 invalid_request |
بدون Telegram |
| JSON خراب | 400 invalid_json |
بدون پردازش business |
| body بزرگتر از limit اپ | 413 request_too_large |
بدون پردازش business |
| خطای پیشبینینشده | 500 internal_error |
log سرور |
قبل از منطق تطبیق، هر transaction نشستهای منقضی را حذف و expiration handler را اجرا میکند.
flowchart TD
A[درخواست معتبر api/sms] --> B[حذف نشستهای منقضی]
B --> C[شمارههای یکتای payload که نشست فعال دارند]
C --> D{تعداد match}
D -->|صفر| E[Audit: no_active_session بدون ذخیره کد]
E --> F[200 success=true delivered=false]
D -->|بیش از یک| G[Audit: ambiguous_active_sessions بدون ذخیره کد]
G --> H[حفظ همه نشستها]
H --> I[200 success=true delivered=false reason=ambiguous]
D -->|دقیقاً یک| J[ارسال کد به چت خصوصی requester]
J --> K{ارسال Telegram موفق؟}
K -->|خیر| L[Audit: private_delivery_failed بدون ذخیره کد]
L --> M[حفظ نشست و 502 برای retry]
K -->|بله| N[حذف نشست و Audit: delivered]
N --> O[لغو timer]
O --> P[افزایش successfulDeliveries اگر شماره هنوز ثبت است]
P --> Q[Reply موفقیت در گروه بدون کد]
Q --> R[200 success=true delivered=true]
- submission با state=
no_active_sessionثبت میشود. - کد عمداً در audit ذخیره نمیشود؛ فقط phones و زمانها ثبت میشوند.
- هیچ پیام خصوصی یا گروهی ارسال نمیشود.
- پاسخ
200 {success:true, delivered:false}است. - اپ آیتم را حذف میکند.
- این حالت معمولاً وقتی رخ میدهد که هر دو شماره configured همزمان نشست فعال دارند و metadata اسلات برای انتخاب قطعی قابل اعتماد نیست.
- کد به هیچ کاربری ارسال نمیشود.
- هر دو نشست active باقی میمانند.
- audit
ambiguous_active_sessionsشامل phones و matchingPhones است، اما خود کد را ذخیره نمیکند. - پاسخ
200 success=true, delivered=false, reason=ambiguous_active_sessionsاست. - اپ آیتم را حذف میکند؛ بنابراین این submission خودکار retry نمیشود.
- بکاند تلاش میکند پیام شامل شماره و کد را به
requesterIdبفرستد. - در شکست Telegram، audit
private_delivery_failedشامل نتیجه و خطای Telegram ثبت میشود، اما خود کد را ذخیره نمیکند. - نشست active و timer آن حفظ میشوند.
- پاسخ HTTP 502 با
telegram_delivery_failedاست. - اپ آیتم را حفظ و طبق backoff retry میکند، ولی فقط تا قبل از انقضای ۶۰ ثانیهای آیتم.
ترتیب آثار:
- کد در چت خصوصی درخواستکننده ارسال میشود.
- نشست از Session Store حذف میشود.
- دو audit با state=
deliveredثبت میشود: یکیsms_submissionو دیگریsession؛ هیچکدام خود کد را نگهداری نمیکنند. - timer نشست لغو میشود.
- اگر شماره هنوز در Phone Store باشد،
successfulDeliveriesافزایش مییابد. - اگر شماره در طول نشست opt-out شده باشد، دوباره ثبت نمیشود و فقط audit
phone_not_registeredثبت میشود. - بکاند در reply پیام اصلی گروه مینویسد: «کد دریافت و ارسال شد.»؛ خود کد در گروه نیست.
- شکست ارسال همین پیام گروهی فقط log میشود و تحویل را rollback نمیکند.
- پاسخ
200 {success:true, delivered:true}به اپ داده میشود.
stateDiagram-v2
[*] --> Rejected: فرمت/شماره/Private نامعتبر
[*] --> Active: ایجاد موفق نشست
Active --> Active: درخواست تکراری رد میشود
Active --> Delivered: یک match و ارسال خصوصی موفق
Active --> Active: ارسال خصوصی کد ناموفق؛ قابل retry
Active --> Active: submission مبهم؛ نشست حفظ میشود
Active --> ExpiredWithoutCode: زمان جاری بزرگتر یا مساوی expiresAt
ExpiredWithoutCode --> ExpirationNotified: پیام خصوصی و گروهی هر دو موفق
ExpiredWithoutCode --> ExpirationNotificationFailed: یکی یا هر دو اعلان ناموفق
Delivered --> [*]
ExpirationNotified --> [*]
ExpirationNotificationFailed --> [*]
Rejected --> [*]
- timer اختصاصی نشست در زمان
expiresAt؛ - هر transaction بعدی Session Store که نشست منقضی را مشاهده کند؛
- startup بکاند بعد از restart.
در لحظه دقیق ۱۲۰ ثانیه، نشست دیگر معتبر نیست (expiresAt <= now).
- نشست از مجموعه active حذف میشود.
- audit
session/expired_without_codeثبت میشود. - پیام
⏳Session expired.به private requester ارسال میشود. - همان متن به صورت reply به پیام اصلی گروه ارسال میشود.
- audit ثانویه ثبت میکند:
expiration_notifiedاگر هر دو موفق باشند؛expiration_notification_failedاگر یکی یا هر دو شکست بخورند، همراه نتیجه و error هر کانال.
- شکست notification باعث بازگشت نشست به active یا retry خودکار اعلان نمیشود.
- فقط در گروه فعال پردازش میشود.
- Phone Store فهرست یکتا را میخواند.
- مرتبسازی:
successfulDeliveriesنزولی؛addedAtصعودی؛- شماره به ترتیب متنی.
- هر ردیف
timesCalledوsuccessfulDeliveriesرا نمایش میدهد. - اگر فهرست خالی باشد، پیام شفاف «هیچ شمارهای ثبت نشده» میدهد.
- نتیجه ارسال پیام با
repliedیاtelegram_failedaudit میشود.
- فقط در گروه فعال پردازش میشود.
- فرمت نامعتبر: reply قالب صحیح + audit
invalid_format. - شماره موجود: رکورد و شمارندههای آن حذف + audit
removed. - شماره ناموجود: reply هشدار + audit
phone_not_found. - حذف شماره نشست فعال آن را لغو نمیکند.
- اگر همان نشست بعداً کد را تحویل دهد، شمارنده موفقیت افزایش نمییابد و شماره دوباره register نمیشود.
- در پیادهسازی فعلی نقش ادمین بررسی نمیشود؛ هر عضوی که بتواند در گروه فعال فرمان بفرستد میتواند opt-out انجام دهد.
- در هر group یا supergroup، حتی گروه غیر فعال، پاسخ میدهد.
- در private chat نادیده گرفته میشود.
- پاسخ شامل ID عددی گروه و reply به پیام اصلی است.
- رخداد audit میشود.
- در private chat، همه چیز جز
/startنادیده گرفته میشود. - در گروه غیر فعال، همه چیز جز
/startو/group_idنادیده گرفته میشود. - در گروه فعال، متن ناشناخته بدون side effect با
success:true, ignored:trueپایان مییابد.
| حالت | ورود | خروج | رفتار SMS |
|---|---|---|---|
OFF |
نصب اولیه، خاموشکردن دستی، revoke شدن RECEIVE_SMS، شکست schedule | enable موفق | Broadcast نادیده گرفته میشود؛ Job لغو است |
ON_DEGRADED |
enable با READ_SMS مفقود یا battery/autostart ناقص | تکمیل readiness یا OFF | SMS زنده ممکن است کار کند؛ recovery/reliability ناقص |
ON_READY |
enable + مجوزها + آمادگی background | revoke/خاموش کردن | دریافت، صف و ارسال کامل |
| state | کد در audit؟ | نشست حفظ میشود؟ | پیام خصوصی؟ | رفتار اپ |
|---|---|---|---|---|
no_active_session |
خیر | موضوعی نیست | خیر | حذف صف؛ retry ندارد |
ambiguous_active_sessions |
خیر | بله، همه | خیر | حذف صف؛ retry ندارد |
private_delivery_failed |
خیر | بله | تلاش ناموفق | حفظ صف و retry |
delivered |
خیر | خیر | بله | حذف صف |
| رخداد | اثر |
|---|---|
| TEST موفق یا پیام قالبدار گروه | ایجاد رکورد فقط اگر جدید باشد |
/code معتبر روی شماره ثبتشده |
timesCalled + 1، حتی اگر نشست بعداً پذیرفته نشود |
| تحویل خصوصی موفق | successfulDeliveries + 1 اگر شماره هنوز ثبت است |
/opt_out |
حذف کامل شماره و شمارندهها؛ نشست فعال دستنخورده |
| TEST مجدد پس از opt-out | شماره با شمارندههای صفر دوباره ساخته میشود |
| پارامتر | مقدار فعلی | معنی |
|---|---|---|
| طول نشست بکاند | ۱۲۰ ثانیه | مهلت دریافت کد از زمان ایجاد نشست |
| عمر آیتم pending اپ | ۶۰ ثانیه | بعد از آن کد stale حذف میشود |
| پنجره recovery Inbox | ۶۰ ثانیه | فقط SMSهای اخیر بررسی میشوند |
| پنجره dedup fingerprint | ۲۴ ساعت | تکرار sender/body/phones enqueue نمیشود |
| JobScheduler backoff پایه | ۳۰ ثانیه، نمایی | retry خطاهای transient |
| HTTP connect timeout اپ | ۱۵ ثانیه | اتصال به بکاند |
| HTTP read timeout اپ | ۲۰ ثانیه | انتظار پاسخ بکاند |
| Telegram send timeout بکاند | ۲۰ ثانیه | هر فراخوانی sendMessage |
| App API JSON limit | ۱۶KB | /api/test و /api/sms |
| Telegram webhook JSON limit | ۲۵۶KB | update تلگرام |
| LocalLog | حداکثر ۵۰ رخداد | UI معمولاً ۱۲ رخداد آخر را نشان میدهد |
| graceful shutdown | ۱۰ ثانیه | سپس force-close |
نتیجه مهم: نشست ۲ دقیقه است، ولی کد در اپ فقط ۱ دقیقه قابلیت ارسال دارد. پس retry اپ نمیتواند تا پایان کامل نشست ادامه پیدا کند.
| خطا/نشانه | محل | رفتار خودکار | اقدام انسانی احتمالی |
|---|---|---|---|
| شماره نامعتبر | اپ/ربات | رد درخواست | اصلاح فرمت 09xxxxxxxxx |
| شماره ثبت نشده | بکاند | نشست ساخته نمیشود | TEST موفق از گوشی یا ثبت قالبدار |
| کاربر Bot را Start نکرده | تلگرام | private setup fail؛ نشست ساخته نمیشود | Start در چت خصوصی و درخواست مجدد |
| نشست تکراری | بکاند | نشست قبلی حفظ | صبر تا تحویل/انقضا یا استفاده از شماره دیگر |
| هر دو شماره نشست فعال دارند | بکاند | کد تحویل نمیشود؛ هر دو نشست حفظ | جلوگیری عملیاتی از همزمانی یا درخواست SMS مجدد با وضعیت بدون ابهام |
| شبکه گوشی قطع | Android Job | انتظار شبکه و retry | برقراری شبکه پیش از ۶۰ ثانیه |
| Secret غلط | بکاند | HTTP 401 و retry تا انقضای صف | rebuild با Secret صحیح |
| Mode غلط | بکاند | HTTP 409 و retry تا انقضای صف | یکسانسازی mode و rebuild/restart |
| Telegram private send fail هنگام تحویل | بکاند + اپ | نشست/صف حفظ و retry | بررسی Telegram/API؛ محدود به پنجره ۶۰ ثانیه |
| اعلان گروه پس از تحویل fail | بکاند | تحویل rollback نمیشود | بررسی audit/log؛ کاربر کد را خصوصی دارد |
| revoke شدن RECEIVE_SMS | اپ | خودکار OFF در onResume | grant مجدد و روشنکردن |
| READ_SMS رد شده | اپ | فقط recovery غیرفعال | grant برای مقاومت در برابر missed broadcast |
| Battery/Autostart ناقص | Android/OEM | فقط هشدار؛ امکان missed SMS | تکمیل readiness |
| JSON store خراب | بکاند | فایل به {} بازسازی میشود |
بازیابی backup؛ نشست/شماره قبلی ممکن است از دست برود |
| session expire | بکاند | حذف + اعلان یکباره | درخواست جدید |
- شماره تلفن؛
- Telegram user ID و نام/username؛
- Group ID و Message ID؛
- زمانهای ایجاد، انقضا و تحویل؛
- خطاهای Telegram/storage؛
- متن فرمان نامعتبر در برخی رخدادها.
- کد OTP در هیچ state از audit ذخیره نمیشود؛ migration نیز کدهای legacy همه stateها را پاک میکند.
- متن کامل SMS و sender از اپ به بکاند ارسال نمیشود.
_historyخودکار prune نمیشود و retention policy باید بیرون از برنامه تعریف شود.- فایلها باید private و backupها محافظت شوند.
- APP_SECRET داخل APK کامپایل میشود و در برابر استخراج از APK یک راز سختافزاری محسوب نمیشود.
- وبهوک فعلی Telegram secret-token header را اعتبارسنجی نمیکند.
- فرمانهای گروه role-based authorization ندارند؛ مرز دسترسی فعلی عضویت/توان ارسال پیام در گروه فعال است.
- پیام قالبدار ثبت شماره، هویت sender را به Bot محدود نمیکند.
این موارد توضیح رفتار فعلیاند و برای threat model یا طراحی نسخه بعد باید به عنوان decision point جدا دیده شوند.
برای جلوگیری از یک نمودار بسیار شلوغ، بهتر است خروجی به هفت نمودار مرتبط تقسیم شود:
- نمودار A — Deployment & Readiness: از
BE-BOOT-01تا Ready و ازAPP-CONFIGتاON_READY. - نمودار B — Phone Registration: کل مسیر
TEST-01..11و ثبت از پیام گروه. - نمودار C — User Code Request: کل مسیر
CODE-REQ-01..16. - نمودار D — SMS Capture: مسیر
SMS-LIVEبه همراه شاخهSMS-RECOVER. - نمودار E — Delivery Queue: state machine صف و مسیر
APP-SEND. - نمودار F — Backend Matching: درخت
SMS-MATCHبا شاخههای ۰، ۱ و ۲ match. - نمودار G — Session Lifecycle: state machine نشست، timer، restart restoration و expiration notification.
- خروجی موفق نمودار B یعنی
Phone=Registeredو ورودی لازم نمودار C است. - خروجی موفق نمودار C یعنی
Session=Activeو trigger اقدام کاربر در دیوار است. - خروجی دیوار/SMS وارد نمودار D میشود.
- خروجی
Queuedنمودار D ورودی نمودار E است. - درخواست HTTP نمودار E وارد نمودار F میشود.
DeliveredیاExpiredنمودار F/G پایان سناریوی اصلی است.ambiguousپایان submission فعلی است، نه پایان نشستها.private_delivery_failedیک loop بین نمودار E و F ایجاد میکند.
- دایره/Terminator: شروع و پایان؛
- مستطیل: عمل قطعی؛
- لوزی: شرط دو یا چند شاخه؛
- استوانه: Phone Store، Session Store یا حافظه محلی؛
- پاکت/Message: پیام Telegram، SMS یا HTTP؛
- Timer event: ۶۰ ثانیه صف یا ۱۲۰ ثانیه نشست؛
- خطچین: notification غیرتراکنشی که شکست آن state اصلی را rollback نمیکند؛
- خط ممتد: مسیر business اصلی؛
- Loop: فقط retry شبکه/private delivery و بازگشت Job.
- مدیر بکاند را در mode مناسب بالا میآورد و webhook فعال است.
- مدیر اپ را نصب، یک یا دو شماره معتبر ثبت، مجوزها را صادر و Status را ON میکند.
- مدیر TEST میزند؛ بکاند پیام تست را به گروه فعال میفرستد و شمارهها در Phone Store ثبت میشوند.
- کاربر ربات را در private chat Start کرده است.
- کاربر در گروه فعال
/code PHONEمیفرستد. - بکاند شماره را معتبر و ثبتشده تشخیص میدهد و
timesCalledرا افزایش میدهد. - بکاند نبود نشست قبلی را تأیید و پیام آمادگی را خصوصی ارسال میکند.
- بکاند نشست active با deadline دو دقیقه میسازد و در گروه اعلام میکند.
- کاربر همان شماره را در دیوار وارد میکند.
- SMS حاوی «دیوار» و کد ششرقمی به گوشی میرسد.
- اپ کد را استخراج، شمارههای کاندید را تعیین، duplicate را بررسی و آیتم را persist میکند.
- Job با وجود شبکه،
POST /api/smsرا با Secret و Mode ارسال میکند. - بکاند دقیقاً یک نشست منطبق پیدا میکند.
- بکاند کد را به requester خصوصی میفرستد.
- پس از موفقیت Telegram، نشست حذف و audit تحویل ثبت میشود.
successfulDeliveriesشماره یک واحد افزایش مییابد.- پیام «کد دریافت و ارسال شد» بدون کد در گروه reply میشود.
- بکاند
success=true, delivered=trueمیدهد. - اپ آیتم صف را حذف و log موفقیت ثبت میکند.
- کاربر کد خصوصی را در دیوار وارد میکند؛ این مرحله خارج از کنترل مستقیم سامانه حاضر است.
سامانه حاضر تا «تحویل خصوصی کد به کاربر» را مدیریت میکند. این موارد خارج از کنترل آناند:
- اینکه دیوار درخواست شماره را قبول کند یا SMS بفرستد؛
- تأخیر یا failure شبکه اپراتور؛
- اینکه کاربر کد را بهموقع و درست در دیوار وارد کند؛
- محدودیتهای force-stop یا OEM که Broadcast و Job را کاملاً متوقف کنند؛
- دسترسپذیری Telegram و شبکه عمومی؛
- صحت عملیاتی عضویتها و مجوزهای کاربران گروه.
پایان موفق فرایند داخلی برابر است با session=delivered و پاسخ delivered=true، نه لزوماً تأیید نهایی ورود کاربر در دیوار.