کاربران نتونن با گذاشتن شماره موبایل/تلفن توی بیو، کپشن پست/ریلز، توضیحاتِ فروشگاه/کالا/پروژه/دوره/پروفایلِ آموزشگاه، بیرون از پلتفرم تماس بدن. یک util مشترک (utils/phoneNumberMasking.js) شمارههای ایرانی رو حتی وقتی رقمها با فاصله/خطفاصله/نقطه از هم جدا شده باشن تشخیص میده و فقط همون بخش رو با * جایگزین میکنه — بدون اینکه به قیمت/سایز/کد سفارش و بقیهی اعدادِ عادی دست بزنه. موقعِ ذخیره (نه نمایش) اعمال میشه، پس هیچ endpointِ خوندنی نیاز به تغییر نداره. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
82 lines
3.7 KiB
JavaScript
82 lines
3.7 KiB
JavaScript
/**
|
||
* 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 }
|