Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Segmentlar: bitta belgi narxni ikki barobar qiladi
- 2.1 GSM-7 va UCS-2
- 2.2 Hisoblash
- 2.3 Kanon o'lchovi: o'zbekcha eslatmalar
- 3. Telefon raqami
- 4. Eskiz API
- 4.1 Oqim
- 4.2 Token: bitta login hammaga
- 4.3 Token eskirsa: 401
- 5. Xatolar va zaxira provayder
- 5.1 Qaysi xatoda qayta urinish
- 5.2 PlayMobile va zaxira
- 5.3 Tezlik
- 6. Callback: yetkazildimi
- 7. Hujumchi nigohi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): matnni GSM'ga moslash
- 3-mashq (qiyin): 401 da bir marta qayta login
- 10. Loyiha qadami: sms.ts moduli
- 11. Real ishda
- Xulosa
- Manbalar
SMS yuborish Node'da: Eskiz va PlayMobile, segmentlar va token
Qisqacha: SMS provayderi (Eskiz, PlayMobile) — HTTP API: login qilib token olasiz, raqam va matnni yuborasiz, yetkazilganini esa provayder callback bilan aytadi. Narx segment bo'yicha: lotin matni GSM-7 da 160 belgigacha bitta SMS, lekin bitta
ʻ,«»yoki—belgisi butun xabarni UCS-2 ga o'tkazadi va chegara 70 ga tushadi. Kodda provayderni interfeys ortiga yashiring, tokenni bitta login bilan bo'lishing, 401 da qayta login qiling va faqat vaqtinchalik xatoda zaxira provayderga o'ting.
Bu darsda
- SMS segmentini hisoblaysiz va o'zbekcha matn nega qimmatlashishini ko'rsatasiz.
- Telefon raqamini bitta formatga keltirasiz.
- Eskiz API oqimini (login, yuborish, callback) va uning tuzoqlarini bilasiz.
- Parallel so'rovlar uchun bitta login (single-flight) naqshini yozasiz.
- Kanon
sms.tsmodulini va uning 7 testini lokal soxta server bilan ishga tushirasiz.
Oldin bilishingiz kerak: Unicode va emoji: matn ichki tuzilishi, Built-in fetch va undici (FormData, AbortSignal.timeout), Promise kombinatorlari, Email yuborish: SMTP va Nodemailer.
Email — bepul va sekin o'qiladi. SMS esa pullik, lekin deyarli darhol ko'riladi va internetsiz ham keladi. Shuning uchun u tasdiqlash kodlari (28-qismda OTP orqali login) va eng muhim eslatmalar uchun.
Diqqat: Bu darsdagi hech bir haqiqiy SMS provayder sinalmagan (2026-10-27): hisob va pul kerak. Eskiz API'si uning rasmiy Postman hujjati bo'yicha (2026-10-27 da o'qildi), sinov esa lokal soxta server (simulyator) bilan. PlayMobile'ning ochiq hujjati topilmadi.
1. Nega bu kerak?
vazifalar foydalanuvchisi Jasur muhim vazifa uchun SMS eslatmani yoqdi. Kunlik 1 000 ta eslatma. Bir SMS narxi — provayder tarifida. Agar har xabar bitta emas, ikkita SMS bo'lib ketsa — xarajat ikki barobar, va buni hech kim sezmaydi, faqat oy oxiridagi hisob.
SMS ishlari backend dasturchining uchta g'amida:
- Narx — segmentlar va belgi to'plami.
- Ishonchlilik — token eskiradi, provayder band bo'ladi, zaxira kerak.
- Suiiste'mol — hujumchi sizning hisobingizdan minglab SMS yubortiradi (SMS pumping).
O'xshatish: SMS — telegramma. So'z soniga qarab haq olinadi, va telegrafchi faqat o'z alifbosidagi harflarni biladi. Begona harf bitta bo'lsa ham, butun telegramma qimmat tarif bilan ketadi.
2. Segmentlar: bitta belgi narxni ikki barobar qiladi
2.1 GSM-7 va UCS-2
SMS ichida 140 bayt joy bor. Matn ikki xil kodlanadi:
| Kodlash | Belgilar | Bitta SMS | Bo'lingan xabarda |
|---|---|---|---|
| GSM-7 | lotin, raqam, oddiy belgilar (128 ta) | 160 | har qism 153 |
| UCS-2 | istalgan Unicode | 70 | har qism 67 |
GSM-7 da har belgi 7 bit — 140 baytga 160 belgi sig'adi. Jadvalda yo'q belgi bitta bo'lsa ham, butun xabar UCS-2 ga o'tadi: har belgi 2 bayt, 70 belgi. Uzun xabar bo'linganda har qism boshida qismlarni birlashtirish uchun sarlavha (UDH) turadi — shuning uchun 153 va 67.
Muhim nozik joy: o'zbek lotin alifbosidagi rasmiy ʻ (U+02BB) GSM jadvalida yo'q. Oddiy apostrof ' esa bor. Kirill — umuman yo'q.
2.2 Hisoblash
Kanon segmentlar funksiyasining JavaScript nusxasi (brauzerda ham ishlaydi):
// GSM 03.38 asosiy jadvali (kengaytma belgilari 2 o'rin egallaydi)
const GSM =
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ " +
"!\"#¤%&'()*+,-./0123456789:;<=>" +
"?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿" +
"abcdefghijklmnopqrstuvwxyzäöñüà";
const GSM_KENG = "^{}\\[~]|€";
function segmentlar(matn) {
let gsm = true;
let uz = 0;
for (const ch of matn) {
if (GSM.includes(ch)) uz += 1;
else if (GSM_KENG.includes(ch)) uz += 2;
else { gsm = false; break; }
}
if (gsm) {
const segment = uz <= 160 ? 1 : Math.ceil(uz / 153);
return { kodlash: "GSM-7", uzunlik: uz, segment };
}
const u = Array.from(matn)
.reduce((a, c) => a + (c.codePointAt(0) > 0xffff ? 2 : 1), 0);
const segment = u <= 70 ? 1 : Math.ceil(u / 67);
return { kodlash: "UCS-2", uzunlik: u, segment };
}
console.log(segmentlar("Kod: 482913. O'zingiz kiriting."));
console.log(segmentlar("Kod: 482913. Oʻzingiz kiriting."));
console.log(segmentlar("Код: 482913"));
console.log(segmentlar("[kod] 482913"));
console.log(segmentlar("a".repeat(161)));
console.log(segmentlar("Oʻ" + "a".repeat(70)));Konsolda:
{ kodlash: 'GSM-7', uzunlik: 31, segment: 1 }
{ kodlash: 'UCS-2', uzunlik: 31, segment: 1 }
{ kodlash: 'UCS-2', uzunlik: 11, segment: 1 }
{ kodlash: 'GSM-7', uzunlik: 14, segment: 1 }
{ kodlash: 'GSM-7', uzunlik: 161, segment: 2 }
{ kodlash: 'UCS-2', uzunlik: 72, segment: 2 }- Birinchi va ikkinchi qator bir xil ko'rinadi — farqi faqat
'vaʻ. Ikkinchisi UCS-2. [kod]— 5 belgi, lekin 6 o'rin:[va]kengaytma jadvalida, har biri 2 o'rin (maxsus "escape" belgisi bilan).- 161 lotin harfi — 2 SMS;
ʻbilan esa 72 belgi allaqachon 2 SMS. - Emoji (U+FFFF dan katta) UCS-2 da 2 o'rin egallaydi (Unicode va emoji — surrogat juftlar).
2.3 Kanon o'lchovi: o'zbekcha eslatmalar
Kanon bir xil ma'noli xabarlarni turli yozuvda sinadi. Har qatorning boshiga — kodlash va SMS soniga qarang, matn esa | dan keyin:
GSM-7 44 belgi → 1 SMS | Tasdiqlash kodi: 482913. Hech kimga aytmang.
GSM-7 67 belgi → 1 SMS | Tasdiqlash kodi: 482913. Uni hech kimga bermang, o'zingiz kiriting.
UCS-2 67 belgi → 1 SMS | Tasdiqlash kodi: 482913. Uni hech kimga bermang, oʻzingiz kiriting.
UCS-2 42 belgi → 1 SMS | Тасдиқлаш коди: 482913. Ҳеч кимга айтманг.
UCS-2 126 belgi → 2 SMS | Eslatma: "Non olish" vazifasi muddati bugun 18:00 da tugaydi. Ilovada
UCS-2 126 belgi → 2 SMS | Eslatma: «Non olish» vazifasi muddati bugun 18:00 da tugaydi. Ilovada
Xulosa: bitta `ʻ`, `«»` yoki `—` (GSM'da yo'q) butun xabarni UCS-2 (70 belgi) ga o'tkazadi; narx segment soniga ko'paytiriladi.(Matn 70 belgida kesib ko'rsatilgan.) Uchinchi qator — 67 belgi, chegaraga uch belgi qoldi: bitta so'z qo'shilsa, ikki SMS. Beshinchi qatorga qarang: oddiy qo'shtirnoq ishlatilgan, lekin xabar baribir UCS-2. Sababi xabar oxirida, kesilgan qismda: … /v/12345 — vazifalar. Bitta — 126 belgilik eslatmani 2 SMS ga aylantirdi. GSM-7 da u bitta bo'lardi.
Tekshirib ko'ring: Dizayner OTP xabarini chiroyli qilish uchun boshiga
🔐qo'shmoqchi. XabarKod: 482913edi. Narx o'zgaradimi?
Javob
Xabar UCS-2 ga o'tadi. Uzunligi 13 o'rin (emoji 2 o'rin), 70 dan kam — hali bitta SMS, narx o'zgarmaydi. Lekin endi matnga 57 o'rin qoldi, GSM-7 da esa 147 edi. Keyin kimdir "Kodni hech kimga aytmang, u 5 daqiqa amal qiladi…" qo'shsa — ikki SMS. Qoida: SMS matnida faqat GSM belgilari, va matnni o'zgartirganda segmentlar testini qo'shing.
3. Telefon raqami
Foydalanuvchi raqamni har xil yozadi: +998 90 123-45-67, (90) 1234567, 901234567. Provayder esa bitta formatni kutadi — Eskiz uchun 998XXXXXXXXX (12 raqam). Kanon funksiyasi:
function raqamNormallash(kirish) {
const r = kirish.replace(/[\s()+-]/g, "");
const toliq = /^\d{9}$/.test(r) ? `998${r}` : r;
return /^998\d{9}$/.test(toliq) ? toliq : undefined;
}
console.log(raqamNormallash("+998 90 123-45-67"));
console.log(raqamNormallash("(90) 123 45 67"));
console.log(raqamNormallash("8 90 123 45 67"));
console.log(raqamNormallash("+7 900 123 45 67"));Konsolda:
998901234567
998901234567
undefined
undefined- 9 raqam — mamlakat kodi qo'shiladi.
- Eski
8 90…formati qabul qilinmaydi: taxmin qilish xato raqamga pul to'lashga olib keladi. Foydalanuvchiga to'g'ri formatni so'rash afzal. - Xorijiy raqam —
undefined. Xalqaro SMS — boshqa narx va boshqa sozlama.
Kanon operator kodlari ro'yxatini (90, 91, 33, 77 …) ataylab yozmadi: yangi kodlar qo'shiladi va qat'iy ro'yxat bir kuni haqiqiy mijozni rad etadi. Raqam haqiqatan foydalanuvchiniki ekanini format emas, tasdiqlash kodi isbotlaydi.
4. Eskiz API
4.1 Oqim
Eskiz (notify.eskiz.uz) rasmiy Postman hujjatiga ko'ra (2026-10-27 da o'qildi):
| Qadam | So'rov | Javob |
|---|---|---|
| Login | POST /api/auth/login (form-data: email, password) |
data.token — 30 kun amal qiladi |
| Yangilash | PATCH /api/auth/refresh |
yangi token |
| Yuborish | POST /api/message/sms/send (mobile_phone, message, from, callback_url) |
{ id, message: "Waiting for SMS provider", status: "waiting" } |
| Callback | provayder → sizning callback_url |
{ request_id, message_id, phone_number, status: "DELIVRD", … } |
sequenceDiagram
participant S as Server
participant E as Eskiz
participant O as Operator
S->>E: login (email, parol)
E-->>S: token (30 kun)
S->>E: sms/send + Bearer token
E-->>S: id, status waiting
E->>O: SMS
O-->>E: yetkazildi
E->>S: callback: DELIVRDDiagrammaga qarang: sms/send javobi "yetkazildi" emas, "navbatga oldim" (waiting). Haqiqiy natija keyin, callback bilan keladi. Bu oldingi darsdagi email bilan bir xil: topshirish va yetkazish — ikki xil voqea.
Hujjatdagi qo'shimcha qoidalar:
from— yuboruvchi nomi (alfa-nom, "nick"); sukutdagi qisqa raqam4546. O'z nomingiz (VAZIFALAR) provayder orqali alohida tasdiqlanadi.- Test rejimi: hisob tasdiqlanmaguncha faqat uchta tayyor matn yuboriladi (
Bu Eskiz dan testva ruscha, inglizcha varianti). - Haqiqiy matnlar provayder kabinetida moderatsiyadan o'tadi.
- Callback holatlari:
DELIVRD,UNDELIV,EXPIRED,REJECTD,DELETED.
Alfa-nom va matn moderatsiyasining aniq tartibi va muddati kanonda sinalmagan — provayder bilan shartnoma va kabinet kerak.
4.2 Token: bitta login hammaga
Eskiz tokeni 30 kun yashaydi. Har SMS oldidan login qilish — ikki barobar so'rov va provayder limitiga tez urilish. Token xotirada saqlanadi. Lekin tuzoq bor: server endigina ishga tushdi va bir vaqtda 10 ta SMS keldi. Har biri "token yo'q" deb o'z login'ini boshlaydi — 10 login.
Yechim — single-flight (bitta parvoz): birinchi chaqiruv login'ni boshlaydi va uning Promise'ini saqlaydi, qolganlari o'sha Promise'ni kutadi:
let loginSoni = 0;
const soxtaLogin = async () => {
loginSoni++;
await new Promise((r) => setTimeout(r, 50));
return `token-${loginSoni}`;
};
let token;
let olinmoqda;
function tokenOl() {
if (token) return Promise.resolve(token);
olinmoqda ??= soxtaLogin()
.then((t) => (token = t))
.finally(() => (olinmoqda = undefined));
return olinmoqda;
}
const natija = await Promise.all(Array.from({ length: 10 }, tokenOl));
console.log(new Set(natija), "login:", loginSoni);Konsolda:
Set(1) { 'token-1' } login: 1O'nta chaqiruv — bitta login, bitta token. ??= — "faqat bo'sh bo'lsa yoz". finally — login xato bilan tugasa ham, keyingi urinish yangi login boshlay olsin. Kanon testi buni lokal soxta Eskiz serverida tasdiqladi: 10 parallel SMS — 1 login.
4.3 Token eskirsa: 401
30 kundan keyin yoki provayder tokenni bekor qilsa, sms/send 401 qaytaradi. Kanon modul qoidasi: bir marta qayta login va takror. Ikkinchi 401 — xato (parol o'zgargan bo'lishi mumkin; cheksiz login sikli provayderni qiynaydi).
5. Xatolar va zaxira provayder
5.1 Qaysi xatoda qayta urinish
SmsXato — kanon modulidagi xato klassi, unda qaytaUrinish bayrog'i bor:
| Holat | qaytaUrinish |
Nega |
|---|---|---|
| 429 (juda ko'p so'rov) | true |
biroz kutib — o'tadi |
| 5xx (provayder xatosi) | true |
vaqtinchalik |
| 401 login'da (parol xato) | false |
sozlamani tuzatish kerak |
| 422 (raqam yoki matn noto'g'ri) | false |
qayta yuborsa ham xuddi shu |
| noto'g'ri raqam (bizning tekshiruv) | false |
so'rov umuman ketmaydi |
Bu oldingi darsdagi SMTP 4xx/5xx qoidasining HTTP varianti.
5.2 PlayMobile va zaxira
PlayMobile — yana bir mahalliy SMS shlyuzi (gateway — sizning HTTP so'rovingizni operatorlar tarmog'idagi SMS'ga aylantiradigan xizmat). Uning ochiq hujjati topilmadi (hamkorlarga beriladi). Kanon modul shaklini uchinchi tomon klientlaridan oldi: Basic auth va JSON messages[] (recipient, message-id, sms.originator, content.text). Bu taxminiy shakl: haqiqiy shartnomada hujjatni tekshiring.
Ikki provayder bo'lsa, birinchisi ishlamay qolganda ikkinchisiga o'tish mumkin — failover (zaxiraga o'tish). Lekin faqat vaqtinchalik xatoda. Matn noto'g'ri bo'lsa (422), ikkinchi provayder ham rad etadi yoki, yomoni, yuborib yuboradi va pul ketadi. Kanon ZaxiraliSms aynan shunday qiladi: kanon testi "Eskiz 503 → PlayMobile; 4xx — zaxiraga o'tmaydi".
5.3 Tezlik
Kanon soxta serverga 200 ta SMS yubordi:
ketma-ket: mediana 1.62 ms; parallel 200: 193 ms; login soni 1 (lokal mock — tarmoq kechikishi yo'q)
Eslatma: haqiqiy provayder tezligi/limiti — SINALMAGAN; navbat + rate limit (#45–#47) bilan yuboriladi.Bu raqamlar faqat bizning kodimiz narxi — tarmoq yo'q. Haqiqiy provayderning soniyadagi limiti shartnomada. SMS'ni HTTP so'rov ichida emas, navbat orqali va tezlik chegarasi bilan yuborish kerak — BullMQ asoslari darsida.
6. Callback: yetkazildimi
Eskiz har SMS natijasini sizning callback_url ga POST qiladi. Kanon modul callback manziliga o'z ID'imizni qo'shadi: …/sms/callback?id=otp-3. Shunda kelgan javobni bazadagi yozuv bilan bog'lash oson. Provayder holati bizning uch holatga aylanadi:
| Provayder | Bizda |
|---|---|
DELIVRD |
yetkazildi |
UNDELIV, EXPIRED, REJECTD, DELETED |
yetkazilmadi |
| boshqasi | kutilmoqda |
Callback — tashqaridan keladigan so'rov, ya'ni webhook. Hujjatda unga imzo yoki maxfiy kalit tasvirlanmagan. Demak manzilni bilgan har kim soxta DELIVRD yubora oladi. Himoya — callback URL'ga taxmin qilib bo'lmaydigan token qo'shish va holatni faqat bazadagi mavjud yozuv uchun yangilash. Webhook xavfsizligi Kiruvchi webhook'larni qabul qilish darsida.
7. Hujumchi nigohi
| Hujum | Natijasi | Himoya |
|---|---|---|
| SMS pumping: bot OTP so'rovini qimmat yo'nalishdagi raqamlarga minglab yuboradi | hisobingizdan pul ketadi | faqat 998, IP va raqam bo'yicha Rate limiting, CAPTCHA (SMS pumping) |
| soxta callback | "yetkazildi" yolg'on | URL'da maxfiy token, faqat mavjud yozuv |
| token logga tushdi | begona sizning nomingizdan SMS yuboradi | tokenni loglamang, .env da parol |
| foydalanuvchi matni SMS'da | fishing havola sizning nomingizdan | SMS matni — faqat shablon + qiymat |
Birinchi qator — haqiqiy biznes xavfi. Hujumchining maqsadi ma'lumot emas, sizning pulingiz: u qimmat tarifli raqamlarga SMS yubortiradi va operator bilan foydani bo'lishadi. Kanon raqamNormallash xorijiy raqamni umuman o'tkazmaydi — bu birinchi qatlam.
8. Ko'p uchraydigan xatolar
| Xato | Natija | To'g'risi |
|---|---|---|
SMS matnida ʻ, «», — |
70 belgi chegarasi, narx 2× | ', ", - |
| har SMS'da login | ikki barobar so'rov | token xotirada, single-flight |
| 401 da cheksiz qayta login | sikl | bir marta |
sms/send javobini "yetkazildi" deyish |
yolg'on holat | callback'ni kuting |
| har xatoda zaxiraga o'tish | ikki marta pul | faqat 429/5xx/tarmoq |
raqamni taxmin qilish (8 90…) |
begona raqamga SMS | aniq format yoki rad |
9. Mashqlar
Mashqlar kurs/mashqlar/29/16-sms/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- GSM-7 da bitta SMS'ga sig'adigan belgilar —
- UCS-2 da —
- Eskiz
sms/sendjavobidagi holat — - Yetkazilganini bildiruvchi callback holati —
- Token eskirganda keladigan HTTP status —
Yechim
- 160 — bo'lingan xabarda 153.
- 70 — bo'lingan xabarda 67.
- waiting — "provayderga topshirildi", yetkazilmagan.
- DELIVRD —
DvaRorasidaEyo'q, hujjatdagidek. - 401 — bir marta qayta login.
2-mashq (o'rta): matnni GSM'ga moslash
gsmgaMoslash(matn) yozing: ʻ ʼ ‘ ’ va teskari tirnoq → ', « » “ ” → ", — – → -, … → .... Natijani va matn faqat ASCII ekanini (/^[\x20-\x7E]*$/) asl va tozalangan matn uchun chiqaring. Ishora: almashtirishlarni [regex, belgi] juftliklari massivida saqlang va for…of bilan aylaning (Almashtirish chuqur).
Yechim
const ALMASH = [
[/[ʻʼ‘’`]/g, "'"],
[/[«»“”]/g, '"'],
[/[—–]/g, "-"],
[/…/g, "..."],
];
function gsmgaMoslash(matn) {
let natija = matn;
for (const [re, belgi] of ALMASH) {
natija = natija.replace(re, belgi);
}
return natija;
}
const asl =
"Eslatma: «Non olish» — bugun 18:00 da. Oʻtkazib yubormang…";
const toza = gsmgaMoslash(asl);
console.log(toza);
console.log(/^[\x20-\x7E]*$/.test(asl), /^[\x20-\x7E]*$/.test(toza));Konsolda:
Eslatma: "Non olish" - bugun 18:00 da. O'tkazib yubormang...
false trueASCII tekshiruvi GSM'dan qat'iyroq: GSM'da é bor, ASCII'da yo'q; ASCII'dagi teskari tirnoq esa GSM'da yo'q — shuning uchun uni ham ' ga almashtirdik. Haqiqiy loyihada tozalangandan keyin segmentlar bilan tekshirish ishonchliroq. Bu almashtirish faqat SMS uchun: ilovaning o'zida va email'da rasmiy ʻ qolsin.
3-mashq (qiyin): 401 da bir marta qayta login
Soxta api obyekti berilgan: login() joriy tokenni qaytaradi va loginlarni sanaydi, yubor(token, matn) token eskirgan bo'lsa { status: 401 }, aks holda { status: 200, id }. yubor(matn) funksiyasini yozing: token bo'lmasa — login; 401 bo'lsa — bir marta yangi login va takror; boshqa status — Error("HTTP <status>"). To'rt holatni sinang: oddiy, yana bir xat (login takrorlanmasin), token almashgan, doim 401. Ishora: ??= (??, ?. va mantiqiy tayinlash).
Yechim
let joriy = "t1";
let loginlar = 0;
const api = {
async login() {
loginlar++;
return joriy;
},
async yubor(token, matn) {
if (token !== joriy) return { status: 401 };
return { status: 200, id: `sms-${matn.length}` };
},
};
let token;
async function yubor(matn) {
token ??= await api.login();
let r = await api.yubor(token, matn);
if (r.status === 401) {
token = await api.login();
r = await api.yubor(token, matn);
}
if (r.status !== 200) throw new Error(`HTTP ${r.status}`);
return r.id;
}
console.log(await yubor("salom"), loginlar);
console.log(await yubor("salom!"), loginlar);
joriy = "t2";
console.log(await yubor("yangi token"), loginlar);
api.yubor = async () => ({ status: 401 });
try {
await yubor("x");
} catch (e) {
console.log(e.message, loginlar);
}Konsolda:
sms-5 1
sms-6 1
sms-11 2
HTTP 401 3Oxirgi holatda login uchinchi marta bo'ldi va to'xtadi — sikl yo'q. Bu yechimda single-flight yo'q: parallel 10 ta yubor token bo'lmaganda 10 ta login qiladi. Kanon modulda ikkalasi birga: #olToken single-flight va 401 da #olToken(true).
10. Loyiha qadami: sms.ts moduli
Kanon SMS'ni modullar/src/sms/sms.ts ga yig'di: raqamNormallash, segmentlar (darsdagi nusxalari), SmsProvayder interfeysi, SmsXato, EskizProvayder, PlayMobileProvayder va ZaxiraliSms. Interfeys — butun modulning o'qi:
export type SmsNatija = {
provayderId: string;
holat: "kutilmoqda" | "yuborildi";
};
export interface SmsProvayder {
readonly nom: string;
yubor(
telefon: string, matn: string, bizningId: string,
): Promise<SmsNatija>;
}bizningId — bizning bazadagi yozuv ID'si. U callback va PlayMobile'ning message-id siga ketadi: javobni yozuvga bog'lash va takrorni aniqlash uchun.
Eskiz provayderi (uzun qatorlarni telefon uchun bo'ldik, ma'nosi kanondagidek):
type EskizSozlama = {
baseUrl: string;
email: string;
parol: string;
from: string;
callbackUrl?: string;
fetchFn?: FetchFn;
kutishMs?: number;
};
/** Eskiz: login → Bearer token (hujjat: 30 kun). 401 bo'lsa — bir
* marta qayta login va takror. Token xotirada. */
export class EskizProvayder implements SmsProvayder {
readonly nom = "eskiz";
#token: string | undefined;
#tokenOlish: Promise<string> | undefined;
readonly #s: EskizSozlama;
constructor(s: EskizSozlama) {
this.#s = s;
}
get #f(): FetchFn {
return this.#s.fetchFn ?? globalThis.fetch;
}
get #signal(): AbortSignal {
return AbortSignal.timeout(this.#s.kutishMs ?? 10_000);
}
/** Parallel so'rovlar bitta login'ni kutadi
* (aks holda 10 SMS = 10 login). */
async #olToken(yangi = false): Promise<string> {
if (!yangi && this.#token) return this.#token;
this.#tokenOlish ??= (async () => {
try {
const fd = new FormData();
fd.append("email", this.#s.email);
fd.append("password", this.#s.parol);
const r = await this.#f(`${this.#s.baseUrl}/api/auth/login`, {
method: "POST", body: fd, signal: this.#signal,
});
if (!r.ok) {
throw new SmsXato(
`Eskiz login: HTTP ${String(r.status)}`,
r.status >= 500,
r.status,
);
}
const j = (await r.json()) as { data?: { token?: string } };
if (!j.data?.token) {
throw new SmsXato("Eskiz login: token yo'q", false);
}
this.#token = j.data.token;
return j.data.token;
} finally {
this.#tokenOlish = undefined;
}
})();
return this.#tokenOlish;
}
async yubor(
telefon: string, matn: string, bizningId: string,
): Promise<SmsNatija> {
const tel = raqamNormallash(telefon);
if (!tel) throw new SmsXato(`noto'g'ri raqam: ${telefon}`, false);
const sorov = async (token: string) => {
const fd = new FormData();
fd.append("mobile_phone", tel);
fd.append("message", matn);
fd.append("from", this.#s.from);
if (this.#s.callbackUrl) {
const id = encodeURIComponent(bizningId);
fd.append("callback_url", `${this.#s.callbackUrl}?id=${id}`);
}
return this.#f(`${this.#s.baseUrl}/api/message/sms/send`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: fd,
signal: this.#signal,
});
};
let r = await sorov(await this.#olToken());
if (r.status === 401) r = await sorov(await this.#olToken(true));
if (!r.ok) {
const matnXato = (await r.text()).slice(0, 200);
const qayta = r.status === 429 || r.status >= 500;
throw new SmsXato(
`Eskiz yuborish: HTTP ${String(r.status)} ${matnXato}`,
qayta, r.status,
);
}
const j = (await r.json()) as { id?: string; status?: string };
if (!j.id) throw new SmsXato("Eskiz javobida id yo'q", false);
return { provayderId: j.id, holat: "kutilmoqda" };
}
}fetchFn— testda soxtafetchberish uchun; sukutda globalfetch.#— xususiy maydonlar (Private # maydonlar): token klassdan tashqariga chiqmaydi.- Har so'rovda
AbortSignal.timeout— provayder javob bermasa, 10 soniyada uziladi (Built-in fetch). - Muvaffaqiyat —
kutilmoqda,yuborildiemas: yetkazishni callback aytadi.
Zaxira (failover) klassi:
export class ZaxiraliSms implements SmsProvayder {
readonly nom = "zaxirali";
readonly #lar: SmsProvayder[];
constructor(...provayderlar: SmsProvayder[]) {
this.#lar = provayderlar;
}
async yubor(
telefon: string, matn: string, bizningId: string,
): Promise<SmsNatija & { provayder: string }> {
let oxirgi: unknown;
for (const p of this.#lar) {
try {
const n = await p.yubor(telefon, matn, bizningId);
return { ...n, provayder: p.nom };
} catch (e) {
oxirgi = e;
if (e instanceof SmsXato && !e.qaytaUrinish) throw e;
}
}
if (oxirgi instanceof Error) throw oxirgi;
throw new Error(String(oxirgi));
}
}ZaxiraliSms o'zi ham SmsProvayder — chaqiruvchi bitta provayder bilanmi yoki zanjir bilanmi ishlayotganini bilmaydi.
Testlar — modullar/test/sms.test.ts, yettita. Ular lokal soxta Eskiz serveri (test/soxta/eskiz.ts) bilan ishlaydi: u hujjatdagi login, token, sms/send, test rejimi va callback'ni taqlid qiladi, xato javoblarni (500, 429, 503, 422) buyruq bilan qaytaradi. Soxta serverning xato tanalari taxminiy — haqiqiy API bilan solishtirilmagan. Docker kerak emas, shuning uchun hammasini biz ishga tushirdik (formatlangan modul nusxasi, tsc va eslint 0, Node 24.21.0):
node --test test/sms.test.ts✔ raqamNormallash (1.1014ms)
✔ segmentlar: ASCII apostrof GSM-7, ʻ va kirill — UCS-2 (1.0977ms)
▶ Eskiz (soxta server)
✔ bir login, parallel 10 SMS; callback DELIVRD → yetkazildi (229.2094ms)
✔ token eskirdi (401) — avtomatik qayta login va takror (29.4544ms)
✔ xatolar: noto'g'ri parol (qayta urinmaydi), 500 va 429 (qayta urinish mumkin), noto'g'ri raqam (14.5322ms)
✔ test rejimi: faqat hujjatdagi test matnlari (15.0635ms)
✔ zaxira: Eskiz 503 → PlayMobile; 4xx — zaxiraga o'tmaydi (14.2487ms)
✔ Eskiz (soxta server) (337.1856ms)
ℹ tests 7
ℹ suites 1
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 566.0282Yetti test, hammasi o'tdi (✔). Millisekundlar har ishga tushishda boshqacha. Commit:
git commit -m "feat(sms): Eskiz va PlayMobile provayderlari, zaxira"vazifalar-api ga ulash 28-qism tugagach: OTP kodlari shu interfeys orqali ketadi, callback marshruti esa yetkazish holatini yozadi.
11. Real ishda
- O'zbekistonda Eskiz va PlayMobile — keng tarqalgan SMS shlyuzlari; ko'p kompaniya ikkalasini zaxira sifatida ulaydi.
- Xalqaro loyihalarda Twilio, Vonage, AWS SNS. Ular uchun ham bir xil interfeys — yangi klass yetarli.
- OTP uchun SMS'ga muqobillar ham bor: Telegram orqali kod (Telegram Gateway), ilova ichidagi push, email. SMS — eng qimmat va eng ko'p hujum qilinadigan kanal.
- Intervyuda: "SMS narxini qanday kamaytirasiz?", "SMS pumping nima?", "Provayder ishlamay qolsa nima qilasiz?"
Xulosa
- GSM-7 — 160 belgi, UCS-2 — 70; bitta
ʻ,«»yoki—xabarni UCS-2 ga o'tkazadi. - Kanon: bitta
—126 belgilik eslatmani 2 SMS qildi. - Raqam —
998XXXXXXXXX, taxmin yo'q; operator kodlari ro'yxati yozilmaydi. - Eskiz: login → token (30 kun),
sms/send→waiting, natija — callback; 401 da bir marta qayta login. - Single-flight: 10 parallel SMS — 1 login.
- Zaxiraga faqat vaqtinchalik xatoda (429, 5xx); SMS pumping'ga qarshi — rate limit va faqat
998.
Keyingi dars: Push bildirishnomalar: FCM va Web Push — brauzerga xabarni server tomonidan yuboramiz: VAPID imzosi, shifrlash va eskirgan obunalarni tozalash.
Manbalar
- Eskiz SMS API (Postman to'plami, 2026-10-27 da o'qildi): https://documenter.gw.postman.com/api/collections/663428/RzfmES4z
- GSM 03.38 belgilar jadvali (3GPP TS 23.038): https://www.3gpp.org/DynaReport/23038.htm
- MDN —
AbortSignal.timeout: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static - Kanon tajribasi (IlmHamroh, 2026-10-27): Eskiz va PlayMobile — lokal soxta server, haqiqiy API sinalmagan; Node 24.21.0. Modul testlari, segment va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!