/** * Masks Iranian mobile/landline phone numbers inside free-text fields (bio, captions, * descriptions, ...) so users can't share off-platform contact numbers through them. * Applied at write time (before saving), not at read time — so the raw number never * even reaches storage. * * Must survive users spacing digits out to dodge a naive "11 digits in a row" regex * (e.g. "0912 345 6789", "0912-345-6789", "09123456789"), while NOT flagging ordinary * numbers (prices, sizes, order codes, percentages) that aren't shaped like a phone * number. The trick: find any run of digits *and common separators* long enough to * matter, strip the separators back out, and only mask it if what's left is actually * shaped like an Iranian phone number (mobile 09xxxxxxxxx, or landline 0[1-8]xxxxxxxx[x], * optionally with a +98/0098/98 country-code prefix). */ const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹' const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩' function toWesternDigits (str) { return str.replace(/[۰-۹٠-٩]/g, (ch) => { const p = PERSIAN_DIGITS.indexOf(ch) if (p !== -1) return String(p) const a = ARABIC_DIGITS.indexOf(ch) return a !== -1 ? String(a) : ch }) } /** Normalizes a leading country-code prefix (+98 / 0098 / 98) down to a plain leading 0. */ function normalizeLeadingCountryCode (digits) { if (digits.startsWith('0098')) return `0${digits.slice(4)}` if (digits.startsWith('98') && digits.length >= 12) return `0${digits.slice(2)}` return digits } /** * Iranian mobile: 09 + 9 digits = 11 digits. * Iranian landline: 0 + area-code digit 1-8 (never 9, that's mobile) + 8-9 more digits, * 10-11 digits total (Tehran's 021 + 8-digit local number is the 11-digit case; several * other cities use a 3-digit area code + 7-digit local number, the 10-digit case). */ function isIranianPhoneNumber (rawDigits) { const digits = normalizeLeadingCountryCode(rawDigits) if (digits.length < 10 || digits.length > 11) return false if (/^09\d{9}$/.test(digits)) return true // کدِ شهرِ ۲ یا ۳ رقمی + شماره‌ی محلیِ ۷ یا ۸ رقمی = جمعاً ۱۰ یا ۱۱ رقم (با صفرِ ابتدایی) if (/^0[1-8]\d{8,9}$/.test(digits)) return true return false } // یک رقم، بعدش حداقل ۸ کاراکترِ رقم/جداکننده (فاصله، خط‌فاصله، نقطه، پرانتز، آندرلاین)، // بعدش یک رقم — یعنی حداقل ۱۰ کاراکتر جمعاً، هم‌اندازه‌ی کوتاه‌ترین شماره‌ی معتبر const CANDIDATE_RUN = /[0-9۰-۹٠-٩][0-9۰-۹٠-٩\s\-.()_+]{8,}[0-9۰-۹٠-٩]/g /** * Replaces any Iranian phone/mobile number found in `text` — including ones with * digits spaced/dashed/dotted apart — with asterisks. Leaves everything else * (prices, sizes, order numbers, short numbers) untouched. */ function maskPhoneNumbers (text) { if (!text || typeof text !== 'string') return text return text.replace(CANDIDATE_RUN, (match) => { const digitsOnly = toWesternDigits(match).replace(/[^0-9]/g, '') if (!isIranianPhoneNumber(digitsOnly)) return match return '*'.repeat(match.length) }) } /** true فقط اگه واقعاً یه شماره‌ی ایرانی توی متن پیدا بشه — برای جاهایی که فقط لازمه‌ی چک‌کردن، نه ماسک‌کردن */ function containsPhoneNumber (text) { if (!text || typeof text !== 'string') return false let found = false text.replace(CANDIDATE_RUN, (match) => { const digitsOnly = toWesternDigits(match).replace(/[^0-9]/g, '') if (isIranianPhoneNumber(digitsOnly)) found = true return match }) return found } module.exports = { maskPhoneNumbers, containsPhoneNumber }