diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ced459f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://mirror-npm.runflare.com diff --git a/controllers/application/account/e2eeController.js b/controllers/application/account/e2eeController.js index 3595972..69ab0da 100644 --- a/controllers/application/account/e2eeController.js +++ b/controllers/application/account/e2eeController.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose') +const crypto = require('crypto') const UserModel = require('../../../models/UserModel') const DeviceLinkSessionModel = require('../../../models/DeviceLinkSessionModel') @@ -152,11 +153,98 @@ const pollDeviceLink = async (req, res, next) => { } } +/** + * رمزِ خودکارِ per-account برای رمزنگاری/بازگشاییِ خودکارِ بک‌آپِ کلیدِ E2EE — به‌جای + * PIN دستیِ کاربر. با یه درخواستِ احرازهویت‌شده به همین اکانت (از هر دستگاهی) قابل‌ + * دریافته، پس بازیابیِ هویت روی دستگاهِ جدید بدون هیچ دخالتِ کاربری انجام می‌شه. + * اولین‌بار که خواسته بشه lazily ساخته می‌شه؛ با یه آپدیتِ شرطی (فقط اگه هنوز null + * باشه) از race بینِ دو دستگاهی که هم‌زمان اولین‌بار درخواستش می‌کنن جلوگیری می‌شه. + */ +const getOrCreateAutoBackupSecret = async (req, res, next) => { + try { + const userId = req.user._id + const existing = await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean() + if (existing?.e2ee_auto_backup_secret) { + return res.json({ secret: existing.e2ee_auto_backup_secret }) + } + + const candidate = crypto.randomBytes(32).toString('base64') + const updated = await UserModel.findOneAndUpdate( + { _id: userId, e2ee_auto_backup_secret: null }, + { $set: { e2ee_auto_backup_secret: candidate } }, + { new: true } + ).select('e2ee_auto_backup_secret').lean() + + // اگه updated خالی برگشت یعنی یه درخواستِ هم‌زمانِ دیگه (دستگاهِ دیگه) بین + // خوندنِ بالا و همین آپدیت، خودش رمز رو ساخته بود — بخونش تا هر دو دستگاه دقیقاً + // یه رمزِ یکسان بگیرن، نه دوتا رمزِ متفاوت + const secret = updated?.e2ee_auto_backup_secret || + (await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean())?.e2ee_auto_backup_secret + + res.json({ secret }) + } catch (error) { + next(error) + } +} + +/** + * ذخیره‌ی بک‌آپِ کلیدِ خصوصیِ رمزنگاری‌شده با PIN/رمزِ انتخابیِ کاربر. سرور فقط + * ciphertext/salt/iv رو می‌بینه — نه خودِ PIN و نه کلیدِ خام. کاربر می‌تونه صدا بزنه + * تا بک‌آپ رو بسازه یا (با یه ciphertext جدید) رمزش رو عوض کنه. + */ +const putKeyBackup = async (req, res, next) => { + try { + const { salt, iv, ciphertext, iterations } = req.body + if ( + !salt || typeof salt !== 'string' || + !iv || typeof iv !== 'string' || + !ciphertext || typeof ciphertext !== 'string' || + typeof iterations !== 'number' || iterations < 100000 + ) { + return res.status(422).json({ error: 'invalid backup payload' }) + } + if (ciphertext.length > 8192 || salt.length > 512 || iv.length > 512) { + return res.status(422).json({ error: 'backup payload too large' }) + } + + await UserModel.findByIdAndUpdate(req.user._id, { + e2ee_key_backup: { salt, iv, ciphertext, iterations } + }) + + res.json({ ok: true }) + } catch (error) { + next(error) + } +} + +/** فقط خودِ کاربر می‌تونه بک‌آپِ خودش رو بخونه — برای بازیابی روی یه دستگاه جدید. */ +const getKeyBackup = async (req, res, next) => { + try { + const user = await UserModel.findById(req.user._id).select('e2ee_key_backup').lean() + res.json({ backup: user?.e2ee_key_backup || null }) + } catch (error) { + next(error) + } +} + +const deleteKeyBackup = async (req, res, next) => { + try { + await UserModel.findByIdAndUpdate(req.user._id, { e2ee_key_backup: null }) + res.json({ ok: true }) + } catch (error) { + next(error) + } +} + module.exports = { putMyPublicKey, getPublicKeyByUserId, startDeviceLink, getDeviceLinkSession, approveDeviceLink, - pollDeviceLink + pollDeviceLink, + getOrCreateAutoBackupSecret, + putKeyBackup, + getKeyBackup, + deleteKeyBackup } diff --git a/controllers/application/analytics/analyticsController.js b/controllers/application/analytics/analyticsController.js new file mode 100644 index 0000000..4724874 --- /dev/null +++ b/controllers/application/analytics/analyticsController.js @@ -0,0 +1,59 @@ +const jwt = require('jsonwebtoken') +const AnalyticsEvent = require('../../../models/AnalyticsEventModel') + +const MAX_BATCH_SIZE = 50 +const VALID_TYPES = new Set(['session_start', 'page_view', 'click']) +const VALID_PLATFORMS = new Set(['web', 'android', 'ios']) + +function decodeUserId(req) { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return null + try { + const decoded = jwt.verify(token, process.env.APP_SECRET) + return decoded?.id || null + } catch { + return null + } +} + +// ورودیِ خام و کوتاه (بدون توکن الزامی — کاربر مهمون هم ردیابی می‌شه)؛ از هر دو کلاینت +// وب (sendBeacon موقع بستن تب) و اپلیکیشن اندروید/iOS به‌صورت دسته‌ای ارسال می‌شه +const trackEvents = async (req, res, next) => { + try { + const userId = decodeUserId(req) + const events = Array.isArray(req.body?.events) ? req.body.events : [] + if (!events.length) return res.status(204).end() + + const docs = events.slice(0, MAX_BATCH_SIZE).reduce((acc, e) => { + if (!e || !VALID_TYPES.has(e.type) || !VALID_PLATFORMS.has(e.platform)) { + return acc + } + if (!e.sessionId || typeof e.sessionId !== 'string') return acc + + acc.push({ + userId, + sessionId: e.sessionId, + type: e.type, + path: typeof e.path === 'string' ? e.path.slice(0, 500) : null, + durationMs: + typeof e.durationMs === 'number' && e.durationMs >= 0 + ? Math.min(e.durationMs, 24 * 60 * 60 * 1000) + : null, + label: typeof e.label === 'string' ? e.label.slice(0, 200) : null, + platform: e.platform, + device: typeof e.device === 'string' ? e.device.slice(0, 100) : null, + browser: typeof e.browser === 'string' ? e.browser.slice(0, 100) : null, + os: typeof e.os === 'string' ? e.os.slice(0, 100) : null + }) + return acc + }, []) + + if (docs.length) await AnalyticsEvent.insertMany(docs, { ordered: false }) + + res.status(204).end() + } catch (error) { + next(error) + } +} + +module.exports = { trackEvents } diff --git a/controllers/application/chatRooms/chatRoomController.js b/controllers/application/chatRooms/chatRoomController.js index e6ab585..0b55296 100644 --- a/controllers/application/chatRooms/chatRoomController.js +++ b/controllers/application/chatRooms/chatRoomController.js @@ -645,7 +645,33 @@ const putRoomE2eeKeys = async (req, res, next) => { }) } - if (typeof e2ee_enabled === 'boolean') { + // فعال‌سازیِ اولیه‌ی E2EE روم باید atomic باشه — بدون این گارد، اگه دو عضو تقریباً + // هم‌زمان (هرکدوم با یه کلیدِ تصادفیِ متفاوت) اولین‌بار بخوان روم رو فعال کنن، هر دو + // findById/save رو با موفقیت رد می‌کردن و ذخیره‌ی دومی wrap های اولی رو override + // می‌کرد — نتیجه یه ناهم‌خوانیِ دائمیِ کلید بین اعضا بود؛ دقیقاً همون باگی که باعث + // شد E2EE روم‌ها موقتاً به‌طور کامل غیرفعال بشه به‌جای رفعِ ریشه‌ای + if (e2ee_enabled === true && !room.e2ee_enabled) { + const claimed = await ChatRoomModel.findOneAndUpdate( + { _id: roomId, e2ee_enabled: { $ne: true } }, + { $set: { e2ee_enabled: true } }, + { new: true } + ) + if (!claimed) { + // یکی دیگه هم‌زمان برنده شد — این درخواست نباید wrapped_keys خودش (که با + // کلیدِ بازنده ساخته شده) رو ثبت کنه؛ کلاینت باید myWrappedKey تازه (متعلق به + // کلیدِ برنده) رو بخونه و کلیدِ محلیِ خودش رو دور بریزه + const fresh = await ChatRoomModel.findById(roomId) + const freshWraps = Array.isArray(fresh?.e2ee_wrapped_keys) ? fresh.e2ee_wrapped_keys : [] + const myFreshWrap = freshWraps.find((w) => String(w.userId) === userId) + return res.status(409).json({ + error: 'room e2ee already enabled by another member', + code: 'E2EE_ALREADY_ENABLED', + e2ee_enabled: true, + myWrappedKey: myFreshWrap?.wrappedKey || null + }) + } + room.e2ee_enabled = true + } else if (typeof e2ee_enabled === 'boolean') { room.e2ee_enabled = e2ee_enabled } diff --git a/controllers/application/payment/paymentControllerWeb.js b/controllers/application/payment/paymentControllerWeb.js index 8d08440..0f061fb 100644 --- a/controllers/application/payment/paymentControllerWeb.js +++ b/controllers/application/payment/paymentControllerWeb.js @@ -980,7 +980,7 @@ const handleOfferPaymentCallback = async (req, res) => { const offerPriceToman = amount / 10 const receiverShare = Math.round(offerPriceToman * OFFER_COMMISSION_RATE) - // سهم گیرنده فقط با پاسخ‌گویی ظرف ۲۴ ساعت به اولین پیام دایرکت واریز می‌شود + // سهم گیرنده فقط با پاسخ‌گویی ظرف ۱۲ ساعت به اولین پیام دایرکت واریز می‌شود // (نگاه کنید به trackOfferMessageForCommission که موقع ارسال پیام صدا زده می‌شود) const offer = await OfferModel.create({ sender: userId, @@ -989,10 +989,18 @@ const handleOfferPaymentCallback = async (req, res) => { pendingCommissionAmount: receiverShare }) + // صفحه/دیپ‌لینکِ موفقیت برای دکمه‌ی «ارسال پیام» به شناسه (و نام‌کاربریِ) گیرنده + // نیاز داره — قبلاً فقط offerId فرستاده می‌شد و صفحه‌ی وب دنبال یک پارامتر userId + // (که هیچ‌وقت ست نمی‌شد) می‌گشت، پس همیشه «کاربر یافت نشد» می‌داد + const receiverUser = await UserModel.findById(receiverId).select('user_name') + const receiverUsername = receiverUser?.user_name || '' + return isAndroid - ? res.redirect(`modstagram://SuccessPay?type=offer&offerId=${offer._id}`) + ? res.redirect( + `modstagram://SuccessPay?type=offer&offerId=${offer._id}&userId=${receiverId}&username=${encodeURIComponent(receiverUsername)}` + ) : res.redirect( - `${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}` + `${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}&userId=${receiverId}` ) } diff --git a/controllers/application/profile/profileController.js b/controllers/application/profile/profileController.js index 1fa568e..fd4c9e2 100644 --- a/controllers/application/profile/profileController.js +++ b/controllers/application/profile/profileController.js @@ -139,8 +139,18 @@ const getProfile = async (req, res, next) => { personal_style: user.personal_style, job: user.job, income_range: user.income_range, - moral_traits: user.moral_traits, personal_details_public: user.personal_details_public === true, + relationship_goal: user.relationship_goal, + seeking_gender: user.seeking_gender, + desired_age_range: user.desired_age_range, + personality_traits: user.personality_traits, + lifestyle: user.lifestyle, + interests: user.interests, + values: user.values, + relationship_view: user.relationship_view, + geo_preference: user.geo_preference, + children_status: user.children_status, + smoking_status: user.smoking_status, profile_image: user.profile_image, lat: user.lat, lng: user.lng, diff --git a/controllers/application/stories/storyController.js b/controllers/application/stories/storyController.js index 8082f83..7d6bb9b 100644 --- a/controllers/application/stories/storyController.js +++ b/controllers/application/stories/storyController.js @@ -13,8 +13,10 @@ const { filterStoryUserIdsByPrivateProfile } = require('../../../utils/privatePr const { sendStoryReactionMessage } = require('../../../utils/storyChatMessage') const { enrichMessage, emitChatEvents } = require('../../../helpers/chatHelpers') const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity') +const { STORY_PHOTO_FILTER_NAMES, applyStoryPhotoFilter } = require('../../../utils/storyPhotoFilters') const LINK_OVERLAY_KINDS = ['pkg', 'project', 'billboard', 'shop', 'shopproduct', 'booking'] +const OVERLAY_FONT_FAMILIES = ['light', 'regular', 'medium', 'bold', 'black'] const STORY_TTL_MS = 24 * 60 * 60 * 1000 @@ -71,12 +73,81 @@ async function sanitizeOverlays (raw, userId) { continue } + if (o.type === 'mention') { + if (!o.mention_user_id || !mongoose.Types.ObjectId.isValid(o.mention_user_id)) continue + // برخلاف لینک، مالکیت لازم نیست (منشن‌کردن یعنی تگ‌کردن کاربرِ دیگه) — فقط باید + // کاربر واقعاً وجود داشته باشه؛ username از خودِ دیتابیس گرفته می‌شه، نه از کلاینت + // (که ممکنه دستکاری‌شده/قدیمی باشه) + const mentioned = await UserModel.findById(o.mention_user_id).select('_id user_name').lean() + if (!mentioned) continue + result.push({ + ...base, + type: 'mention', + text: '', + font_size: clamp01(o.font_size, 0.06) || 0.06, + color: '#ffffff', + background: '', + align: 'center', + mention_user_id: String(mentioned._id), + mention_username: mentioned.user_name + }) + continue + } + + if (o.type === 'hashtag') { + const tag = typeof o.text === 'string' ? o.text.trim().replace(/^#/, '').slice(0, 60) : '' + if (!tag) continue + result.push({ + ...base, + type: 'hashtag', + text: tag, + font_size: clamp01(o.font_size, 0.06) || 0.06, + color: '#ffffff', + background: '', + align: 'center' + }) + continue + } + + if (o.type === 'location') { + const place = typeof o.text === 'string' ? o.text.trim().slice(0, 100) : '' + if (!place) continue + result.push({ + ...base, + type: 'location', + text: place, + font_size: clamp01(o.font_size, 0.06) || 0.06, + color: '#ffffff', + background: '', + align: 'center' + }) + continue + } + + if (o.type === 'sticker') { + // فقط رشته‌ی خودِ ایموجی ذخیره می‌شه — یه ایموجی با اجزای ترکیبی (پرچم/تن‌پوست/ZWJ) + // می‌تونه چند واحدِ UTF-16 باشه، برای همین سقف کمی بازتره تا کاراکترهای عادی + const emoji = typeof o.text === 'string' ? o.text.trim().slice(0, 16) : '' + if (!emoji) continue + result.push({ + ...base, + type: 'sticker', + text: emoji, + font_size: clamp01(o.font_size, 0.09) || 0.09, + color: '#ffffff', + background: '', + align: 'center' + }) + continue + } + if (typeof o.text !== 'string' || !o.text.trim().length) continue result.push({ ...base, type: 'text', text: String(o.text).slice(0, 300), font_size: clamp01(o.font_size, 0.06) || 0.06, + font_family: OVERLAY_FONT_FAMILIES.includes(o.font_family) ? o.font_family : 'bold', color: typeof o.color === 'string' ? o.color.slice(0, 32) : '#ffffff', background: typeof o.background === 'string' ? o.background.slice(0, 32) : '', align: ['left', 'center', 'right'].includes(o.align) ? o.align : 'center', @@ -100,6 +171,32 @@ async function resolveViewerId (req) { } } +// پیش‌نمایش/اعمال فیلترِ رنگ‌ونور (وارم/کول/سیاه‌وسفید/زنده/محو/قدیمی) روی عکسِ گرفته‌شده +// از دوربین، قبل از رفتن به مرحله‌ی ادیتور اورلی — سمتِ سرور با sharp (رایگان، بدون +// نیاز به کتابخونه‌ی بومیِ اندروید/iOS که ریسکِ بیلد داشت) +const applyPhotoFilter = async (req, res) => { + try { + const { file, filter } = req.body + if (!file?.data) { + return res.status(400).json({ error: true, message: 'فایل عکس الزامی است' }) + } + if (!STORY_PHOTO_FILTER_NAMES.includes(filter)) { + return res.status(400).json({ error: true, message: 'فیلتر نامعتبر است' }) + } + const inputBuffer = Buffer.from(file.data.split(',')[1] || file.data, 'base64') + const outputBuffer = await applyStoryPhotoFilter(inputBuffer, filter) + return res.status(200).json({ + file: { + data: `data:image/jpeg;base64,${outputBuffer.toString('base64')}`, + type: 'image/jpeg' + } + }) + } catch (err) { + console.error('applyPhotoFilter:', err) + return res.status(500).json({ error: true, message: 'خطا در اعمال فیلتر' }) + } +} + const createStoryBase64 = async (req, res) => { try { const userId = req.user._id @@ -632,6 +729,7 @@ const sendStoryToUsers = async (req, res, next) => { } module.exports = { + applyPhotoFilter, createStoryBase64, createStoryMultipart, createStoryFromPost, diff --git a/controllers/application/users/getUserController.js b/controllers/application/users/getUserController.js index 50a3fc3..3a378f1 100644 --- a/controllers/application/users/getUserController.js +++ b/controllers/application/users/getUserController.js @@ -1583,25 +1583,27 @@ const getPostsWeb = async (req, res, next) => { }) } + // پروفایل یک کاربر خاص به‌صورت ریلز: همه‌ی پست‌های pool از یک نویسنده‌ن، پس رتبه‌بندی + // شخصی‌سازی‌شده (paginatePersonalizedExplore) بی‌معنیه — diversifyByAuthor توش با + // maxPerPage=1 فقط اولین پست رو نگه می‌داره و بقیه‌ی پست‌های همون یک نویسنده رو با + // ترتیبِ امتیازدهی‌شده (نه زمانی) می‌ریزه ته لیست، یعنی «پست بعدی» دیگه پست + // بعدیِ واقعیِ همون کاربر نیست. اینجا فقط seed post میره اول، بقیه دقیقاً با همون + // ترتیب زمانی/پین‌شده‌ی posts (که بالاتر مرتب شده) می‌مونن if (feedMode === 'reels' && _id && seedPostId && posts.length > 1) { - const { posts: paginatedPosts, totalItems, isColdStart } = - await paginatePersonalizedExplore({ - posts, - usersById, - page, - limit, - userId: decodedToken?.id, - seedPostId, - reelsMode: true, - blendFresh: false, - cacheScope: `reels-profile:${_id}` - }) + const seedIndex = posts.findIndex((p) => String(p._id) === String(seedPostId)) + const orderedPosts = seedIndex > 0 + ? [posts[seedIndex], ...posts.slice(0, seedIndex), ...posts.slice(seedIndex + 1)] + : posts + + const startIndex = (page - 1) * limit + const endIndex = page * limit + const totalItems = orderedPosts.length + const paginatedPosts = orderedPosts.slice(startIndex, endIndex) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, - totalItems, - feedMeta: { isColdStart: !!isColdStart } + totalItems }) } @@ -1919,7 +1921,9 @@ const getSingleUserWeb = async (req, res, next) => { eye_color: user.eye_color, hair_color: user.hair_color, // «مشخصات فردی» فقط وقتی خودِ کاربر با تیک «نمایش مشخصات من برای عموم» موافقت کرده - // برای بقیه نمایش داده می‌شه — برای خودِ صاحب پروفایل صرف‌نظر از این تیک همیشه برمی‌گرده + // برای بقیه نمایش داده می‌شه — برای خودِ صاحب پروفایل صرف‌نظر از این تیک همیشه برمی‌گرده. + // فیلدهای ترجیحاتِ پارتنریابی (relationship_goal تا smoking_status) بعداً به فرمِ + // ویرایشِ مشخصاتِ فردی اضافه شدن ولی اینجا جا مونده بودن — الان همراهِ بقیه برمی‌گردن ...(user.personal_details_public === true || isSelf ? { marital_status: user.marital_status, @@ -1930,7 +1934,15 @@ const getSingleUserWeb = async (req, res, next) => { personal_style: user.personal_style, job: user.job, income_range: user.income_range, - moral_traits: user.moral_traits + relationship_goal: user.relationship_goal, + personality_traits: user.personality_traits, + lifestyle: user.lifestyle, + interests: user.interests, + values: user.values, + relationship_view: user.relationship_view, + geo_preference: user.geo_preference, + children_status: user.children_status, + smoking_status: user.smoking_status } : {}), personal_details_public: user.personal_details_public === true, diff --git a/controllers/application/users/nearbyUsersController.js b/controllers/application/users/nearbyUsersController.js index 7f799e6..94a0c84 100644 --- a/controllers/application/users/nearbyUsersController.js +++ b/controllers/application/users/nearbyUsersController.js @@ -83,16 +83,18 @@ const getNearbyUsers = async (req, res, next) => { ) const blockedMe = new Set((blockerIds || []).map((id) => String(id))) + // فقط کاربرانی که «نمایش لوکیشن به دیگران» رو فعال کردن و دسترسی موقعیت مکانی + // رو به اپ داده‌ن (یعنی lat/lng واقعی دارن) باید توی رادار/لیست/نقشه دیده بشن const candidates = await UserModel.find({ _id: { $ne: userId }, - show_location: true, account_status: { $ne: 'deactivated' }, + show_location: true, lat: { $nin: [null, ''] }, lng: { $nin: [null, ''] }, user_name: { $ne: null } }) .select( - 'user_name first_name last_name profile_image is_verified verify_badge is_Register lat lng blocked_users' + 'user_name first_name last_name profile_image is_verified verify_badge is_Register show_location lat lng blocked_users' ) .lean() @@ -120,6 +122,9 @@ const getNearbyUsers = async (req, res, next) => { is_verified: user.is_verified, verify_badge: user.verify_badge, is_Register: user.is_Register, + // فقط با تیکِ «نمایش لوکیشن به دیگران» باید روی نقشه‌ی واقعی قرار بگیرن — بدونش + // فقط توی رادار نشون داده می‌شن (تصمیم روی فرانت‌اند، اینجا فقط پرچمش رو می‌فرستیم) + show_location: user.show_location === true, distanceMeters: Math.round(distanceMeters) }) } diff --git a/controllers/application/users/partnerSearchController.js b/controllers/application/users/partnerSearchController.js index c5ebfd8..59e05a2 100644 --- a/controllers/application/users/partnerSearchController.js +++ b/controllers/application/users/partnerSearchController.js @@ -13,6 +13,17 @@ function escapeRegex (value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } +/** Multi-select query params (relationship_goal, personality_traits, lifestyle, + * interests, values, children_status) are sent as a single comma-separated + * string, e.g. `?personality_traits=kind,loyal` — this is the one convention + * the RN/web clients must use for every array-type dating-profile filter. + * Empty/blank items are dropped; returns [] for an empty/absent param. */ +function parseCsvParam (value) { + if (value === undefined || value === null || value === '') return [] + const raw = Array.isArray(value) ? value.join(',') : String(value) + return raw.split(',').map((v) => v.trim()).filter((v) => v.length > 0) +} + /** $expr clauses for an age range, computed from `birthday` — mirrors * utils/partnerMatch.js's ageFromBirthday so live search and later alert * matching agree on the same age. */ @@ -31,7 +42,10 @@ function ageRangeExprClauses (ageMin, ageMax) { /** Turns raw query params into the persisted alert-criteria shape consumed * by utils/partnerMatch.js's matchesCriteria — used both for the immediate - * search and when a search is saved as an alert, so both always agree. */ + * search and when a search is saved as an alert, so both always agree. + * Multi-select dating-profile filters (relationship_goal, personality_traits, + * lifestyle, interests, values) use a single comma-separated query param, + * e.g. `?personality_traits=kind,loyal` — see parseCsvParam. */ async function resolveCriteriaFromQuery (query) { const criteria = {} const { @@ -39,7 +53,10 @@ async function resolveCriteriaFromQuery (query) { heightMin, heightMax, weightMin, weightMax, sizeMin, sizeMax, hair_color, eye_color, marital_status, residence_type, vehicle_type, personal_style, - income_range, job, country, province, city + income_range, job, country, province, city, + relationship_goal, seeking_gender, personality_traits, + lifestyle, interests, values, relationship_view, geo_preference, + children_status, smoking_status } = query || {} if (gender) criteria.gender = gender @@ -60,6 +77,26 @@ async function resolveCriteriaFromQuery (query) { if (income_range) criteria.income_range = income_range if (job) criteria.job = String(job).trim() + // desired_age_range is a stored preference field only (echoed back via + // PARTNER_RESULT_FIELDS) — it is NOT translated into a search filter here; + // clients bucket it into ageMin/ageMax themselves before calling search. + const relationshipGoalList = parseCsvParam(relationship_goal) + if (relationshipGoalList.length) criteria.relationship_goal = relationshipGoalList + if (seeking_gender) criteria.seeking_gender = seeking_gender + const personalityTraitsList = parseCsvParam(personality_traits) + if (personalityTraitsList.length) criteria.personality_traits = personalityTraitsList + const lifestyleList = parseCsvParam(lifestyle) + if (lifestyleList.length) criteria.lifestyle = lifestyleList + const interestsList = parseCsvParam(interests) + if (interestsList.length) criteria.interests = interestsList + const valuesList = parseCsvParam(values) + if (valuesList.length) criteria.values = valuesList + if (relationship_view) criteria.relationship_view = relationship_view + if (geo_preference) criteria.geo_preference = geo_preference + const childrenStatusList = parseCsvParam(children_status) + if (childrenStatusList.length) criteria.children_status = childrenStatusList + if (smoking_status) criteria.smoking_status = smoking_status + if (country) { const countryFind = await CountryModel.findOne({ id: country }, { _id: 0 }) if (countryFind) criteria.country_id = countryFind.id @@ -99,6 +136,22 @@ function criteriaToMongoFilter (criteria) { if (criteria.personal_style) filter.personal_style = criteria.personal_style if (criteria.income_range) filter.income_range = criteria.income_range if (criteria.job) filter.job = { $regex: escapeRegex(criteria.job), $options: 'i' } + + // Multi-select dating-profile fields: candidate matches if their stored + // array overlaps ANY of the searcher's selected values ($in semantics). + if (criteria.relationship_goal?.length) filter.relationship_goal = { $in: criteria.relationship_goal } + if (criteria.personality_traits?.length) filter.personality_traits = { $in: criteria.personality_traits } + if (criteria.lifestyle?.length) filter.lifestyle = { $in: criteria.lifestyle } + if (criteria.interests?.length) filter.interests = { $in: criteria.interests } + if (criteria.values?.length) filter.values = { $in: criteria.values } + + // Single-select dating-profile fields: exact match. + if (criteria.seeking_gender) filter.seeking_gender = criteria.seeking_gender + if (criteria.relationship_view) filter.relationship_view = criteria.relationship_view + if (criteria.geo_preference) filter.geo_preference = criteria.geo_preference + if (criteria.children_status?.length) filter.children_status = { $in: criteria.children_status } + if (criteria.smoking_status) filter.smoking_status = criteria.smoking_status + if (criteria.country_id) filter['country.id'] = criteria.country_id if (criteria.province_id) filter['province.id'] = criteria.province_id if (criteria.city_id) filter['city.id'] = criteria.city_id @@ -115,7 +168,9 @@ function criteriaToMongoFilter (criteria) { const PARTNER_RESULT_FIELDS = '_id user_name first_name last_name profile_image is_verified verify_badge is_Register ' + 'gender height weight size eye_color hair_color marital_status residence_type residence_area ' + - 'vehicle_type vehicle_details personal_style job income_range moral_traits birthday country province city blocked_users' + 'vehicle_type vehicle_details personal_style job income_range birthday country province city blocked_users ' + + 'relationship_goal seeking_gender desired_age_range personality_traits lifestyle interests values ' + + 'relationship_view geo_preference children_status smoking_status' /** GET /users/partner-search — filters as query params (see resolveCriteriaFromQuery). * Personal-details fields are included in the response ONLY here, by design diff --git a/controllers/application/verify/addressController.js b/controllers/application/verify/addressController.js index 4f55515..95738e1 100644 --- a/controllers/application/verify/addressController.js +++ b/controllers/application/verify/addressController.js @@ -53,8 +53,11 @@ const setAddress = async (req, res, next) => { if (country) { user.country = { id: country.id, name: country.name_fa || country.name, iso2: country.iso2, slug: country.slug } } - user.province = province - user.city = city + // مثل country بالا — نام فارسی (name_fa) رو ترجیح بده، وگرنه استان/شهر با نام + // انگلیسیِ خام (name) ذخیره می‌شد؛ قبلاً کل داکیومنت خام province/city (که name_fa + // جدا داشت ولی field نمایشیِ name همچنان انگلیسی بود) مستقیم ذخیره می‌شد + user.province = { id: province.id, name: province.name_fa || province.name, slug: province.slug } + user.city = { id: city.id, name: city.name_fa || city.name, slug: city.slug } user.address = address user.lat = lat user.lng = lng @@ -125,8 +128,11 @@ const updateAddress = async (req, res, next) => { if (country) { user.country = { id: country.id, name: country.name_fa || country.name, iso2: country.iso2, slug: country.slug } } - user.province = province - user.city = city + // مثل country بالا — نام فارسی (name_fa) رو ترجیح بده، وگرنه استان/شهر با نام + // انگلیسیِ خام (name) ذخیره می‌شد؛ قبلاً کل داکیومنت خام province/city (که name_fa + // جدا داشت ولی field نمایشیِ name همچنان انگلیسی بود) مستقیم ذخیره می‌شد + user.province = { id: province.id, name: province.name_fa || province.name, slug: province.slug } + user.city = { id: city.id, name: city.name_fa || city.name, slug: city.slug } user.address = address user.lat = lat user.lng = lng diff --git a/controllers/application/verify/genderController.js b/controllers/application/verify/genderController.js index 2371eca..19e69e8 100644 --- a/controllers/application/verify/genderController.js +++ b/controllers/application/verify/genderController.js @@ -41,6 +41,49 @@ const setGender = async (req, res, next) => { } } -module.exports = { - setGender +// نسخه‌ی PATCH — عیناً همون منطقِ setGender، فقط طبق قراردادِ بقیه‌ی فیلدهای +// «مشخصات فردی» (نگاه کنید personalDetailsController.js: set=POST برای ثبت‌نامِ +// اولیه، update=PATCH برای ویرایش از تنظیمات) که settings/edit/gender/page.tsx +// باید صداش بزنه، نه POST که مخصوص ویزارد ثبت‌نامه +const updateGender = async (req, res, next) => { + try { + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const { gender } = req.body + if (!gender) { + return res.status(422).json({ + error: true, + message: 'اطلاعات ورودی اشتباه است' + }) + } + if (!['male', 'female'].includes(gender)) { + return res.status(422).json({ + error: true, + message: 'جنسیت معتبر نیست' + }) + } + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const user = await UserModel.findById(decodedToken.id) + if (!user) { + return res.status(404).json({ + error: true, + message: 'کاربری یافت نشد' + }) + } + + user.gender = gender.toLowerCase() + await user.save() + + return res.json({ + message: 'جنسیت با موفقیت به‌روزرسانی شد', + gender: user.gender + }) + } catch (error) { + next(error) + } +} + +module.exports = { + setGender, + updateGender } diff --git a/controllers/application/verify/personalDetailsController.js b/controllers/application/verify/personalDetailsController.js index 537db76..a3a76f1 100644 --- a/controllers/application/verify/personalDetailsController.js +++ b/controllers/application/verify/personalDetailsController.js @@ -2,11 +2,118 @@ const UserModel = require('../../../models/UserModel') const jwt = require('jsonwebtoken') -const MARITAL_STATUS_VALUES = ['single', 'divorced'] +const MARITAL_STATUS_VALUES = ['single', 'divorced', 'widowed', 'not_in_relationship', 'prefer_not_to_say'] const RESIDENCE_TYPE_VALUES = ['owner', 'tenant'] const VEHICLE_TYPE_VALUES = ['none', 'car', 'motorcycle'] -const PERSONAL_STYLE_VALUES = ['formal', 'sporty'] -const INCOME_RANGE_VALUES = ['under_5m', '5m_10m', '10m_20m', 'over_20m'] +const PERSONAL_STYLE_VALUES = ['casual', 'classic', 'minimal', 'formal', 'street', 'romantic'] +const INCOME_RANGE_VALUES = [ + 'under_40m', '40m_50m', '50m_60m', '60m_70m', '70m_80m', + '80m_90m', '90m_100m', '100m_150m', '150m_200m', 'over_200m' +] + +const RELATIONSHIP_GOAL_VALUES = [ + 'friendship', + 'serious_partner', + 'marriage_intro', + 'emotional_relationship', + 'travel_event_companion', + 'social_networking', + 'not_sure' +] +const SEEKING_GENDER_VALUES = ['female', 'male', 'both', 'not_important'] + +const DESIRED_AGE_RANGE_VALUES = ['18_24', '25_30', '31_35', '36_40', '41_50', 'over_50', 'not_important'] + +const PERSONALITY_TRAITS_VALUES = [ + 'kind', + 'loyal', + 'funny', + 'calm', + 'energetic', + 'responsible', + 'ambitious', + 'emotional', + 'logical', + 'patient', + 'honest', + 'romantic', + 'independent', + 'family_oriented', + 'adventurous', + 'introvert', + 'extrovert' +] + +const LIFESTYLE_VALUES = [ + 'active_healthy', + 'homebody_calm', + 'social_outgoing', + 'busy_career', + 'traveler', + 'night_owl', + 'early_riser', + 'minimalist', + 'luxury_lover' +] + +const INTERESTS_VALUES = [ + 'movies_series', + 'music', + 'books_reading', + 'sports', + 'cooking', + 'photography', + 'travel', + 'nature', + 'gaming', + 'art_painting', + 'fashion', + 'pets', + 'dancing', + 'language_learning' +] + +const VALUES_VALUES = [ + 'family', + 'career_growth', + 'religion_spirituality', + 'personal_freedom', + 'financial_stability', + 'health_fitness', + 'personal_growth', + 'excitement_new_experiences' +] + +const RELATIONSHIP_VIEW_VALUES = [ + 'traditional', + 'modern', + 'long_term', + 'friendship_first', + 'marriage_soon', + 'friendship_only_for_now' +] + +const GEO_PREFERENCE_VALUES = [ + 'same_city_only', + 'nearby_cities', + 'whole_province', + 'whole_iran', + 'even_abroad', + 'online_is_fine' +] + +const CHILDREN_STATUS_VALUES = ['no_children', 'has_children', 'open_to_children', 'not_now', 'prefer_not_to_say'] + +const SMOKING_STATUS_VALUES = ['none', 'sometimes', 'yes', 'doesnt_matter', 'prefer_not_to_say'] + +/** Validates a multi-select field: must be an array and every item must be + * in `allowedValues` (no cap on how many may be selected). Returns a + * Persian error message on failure, or null on success. */ +function validateMultiSelect (value, allowedValues, errorMessage) { + if (!Array.isArray(value)) return errorMessage + if (!value.every((item) => allowedValues.includes(item))) return errorMessage + return null +} /** All fields are optional profile-enrichment fields — only the ones present * in the request body are validated/applied, unlike sizes/cooperation-type @@ -54,12 +161,96 @@ function applyPersonalDetails (user, body) { } user.income_range = body.income_range } - if (body.moral_traits !== undefined) { - user.moral_traits = body.moral_traits - } if (body.personal_details_public !== undefined) { user.personal_details_public = Boolean(body.personal_details_public) } + if (body.relationship_goal !== undefined) { + const error = validateMultiSelect( + body.relationship_goal, RELATIONSHIP_GOAL_VALUES, + 'هدف از آشنایی نامعتبر است' + ) + if (error) return error + user.relationship_goal = body.relationship_goal + } + if (body.seeking_gender !== undefined) { + if (body.seeking_gender !== null && !SEEKING_GENDER_VALUES.includes(body.seeking_gender)) { + return 'جنسیت مورد نظر نامعتبر است' + } + user.seeking_gender = body.seeking_gender + } + if (body.desired_age_range !== undefined) { + if (body.desired_age_range !== null && !DESIRED_AGE_RANGE_VALUES.includes(body.desired_age_range)) { + return 'بازه سنی مورد نظر نامعتبر است' + } + user.desired_age_range = body.desired_age_range + } + if (body.personality_traits !== undefined) { + const error = validateMultiSelect( + body.personality_traits, PERSONALITY_TRAITS_VALUES, + 'ویژگی‌های شخصیتی نامعتبر است' + ) + if (error) return error + user.personality_traits = body.personality_traits + } + if (body.lifestyle !== undefined) { + const error = validateMultiSelect( + body.lifestyle, LIFESTYLE_VALUES, + 'سبک زندگی نامعتبر است' + ) + if (error) return error + user.lifestyle = body.lifestyle + } + if (body.interests !== undefined) { + const error = validateMultiSelect( + body.interests, INTERESTS_VALUES, + 'علایق نامعتبر است' + ) + if (error) return error + user.interests = body.interests + } + if (body.values !== undefined) { + const error = validateMultiSelect( + body.values, VALUES_VALUES, + 'ارزش‌ها نامعتبر است' + ) + if (error) return error + user.values = body.values + } + if (body.relationship_view !== undefined) { + if (body.relationship_view !== null && !RELATIONSHIP_VIEW_VALUES.includes(body.relationship_view)) { + return 'دیدگاه نسبت به رابطه نامعتبر است' + } + user.relationship_view = body.relationship_view + } + if (body.geo_preference !== undefined) { + if (body.geo_preference !== null && !GEO_PREFERENCE_VALUES.includes(body.geo_preference)) { + return 'محدوده جغرافیایی مورد نظر نامعتبر است' + } + user.geo_preference = body.geo_preference + } + if (body.children_status !== undefined) { + const error = validateMultiSelect( + body.children_status, CHILDREN_STATUS_VALUES, + 'وضعیت فرزند نامعتبر است' + ) + if (error) return error + user.children_status = body.children_status + } + if (body.smoking_status !== undefined) { + if (body.smoking_status !== null && !SMOKING_STATUS_VALUES.includes(body.smoking_status)) { + return 'وضعیت مصرف دخانیات نامعتبر است' + } + user.smoking_status = body.smoking_status + } + // فیلد سبک‌وزن برای ثبت تاریخ تولد از همین اندپوینت (بدون نیاز به شبا/کد ملی که + // POST /verify/auth الزامی می‌کنه) — مثلاً وقتی کاربر فقط سن رو توی «مشخصات فردی» + // اضافه می‌کنه، نه کل احراز هویت بانکی/ملی + if (body.birthday !== undefined) { + if (body.birthday !== null && Number.isNaN(Date.parse(body.birthday))) { + return 'تاریخ تولد نامعتبر است' + } + user.birthday = body.birthday + } return null } @@ -123,8 +314,19 @@ const updatePersonalDetails = async (req, res, next) => { personal_style: user.personal_style, job: user.job, income_range: user.income_range, - moral_traits: user.moral_traits, - personal_details_public: user.personal_details_public === true + personal_details_public: user.personal_details_public === true, + relationship_goal: user.relationship_goal, + seeking_gender: user.seeking_gender, + desired_age_range: user.desired_age_range, + personality_traits: user.personality_traits, + lifestyle: user.lifestyle, + interests: user.interests, + values: user.values, + relationship_view: user.relationship_view, + geo_preference: user.geo_preference, + children_status: user.children_status, + smoking_status: user.smoking_status, + birthday: user.birthday }) } catch (error) { next(error) diff --git a/controllers/panel/adminUsers/adminUsersController.js b/controllers/panel/adminUsers/adminUsersController.js new file mode 100644 index 0000000..ca1e04c --- /dev/null +++ b/controllers/panel/adminUsers/adminUsersController.js @@ -0,0 +1,107 @@ +const bcrypt = require('bcryptjs') +const AdminModel = require('../../../models/AdminModel') +const { PANEL_PERMISSIONS, PANEL_PERMISSION_KEYS } = require('../../../utils/panelPermissions') + +function sanitizeAdmin(admin) { + return { + _id: admin._id, + user_name: admin.user_name, + first_name: admin.first_name, + last_name: admin.last_name, + role: admin.role, + permissions: admin.permissions || [], + createdAt: admin.createdAt, + last_online: admin.last_online + } +} + +const listAdmins = async (req, res, next) => { + try { + const admins = await AdminModel.find({}).sort({ createdAt: -1 }) + res.status(200).json({ + admins: admins.map(sanitizeAdmin), + availablePermissions: PANEL_PERMISSIONS + }) + } catch (error) { + next(error) + } +} + +const createAdmin = async (req, res, next) => { + try { + const { user_name, password, first_name, last_name, permissions } = req.body + if (!user_name || !password) { + return res.status(422).json({ error: true, message: 'نام کاربری و رمز عبور الزامی است' }) + } + + const userLow = String(user_name).toLowerCase().trim() + const existing = await AdminModel.findOne({ user_name: userLow }) + if (existing) { + return res.status(409).json({ error: true, message: 'این نام کاربری قبلاً استفاده شده است' }) + } + + const validPermissions = Array.isArray(permissions) + ? permissions.filter((p) => PANEL_PERMISSION_KEYS.has(p)) + : [] + + const hashedPassword = await bcrypt.hash(password, 10) + const admin = await AdminModel.create({ + user_name: userLow, + password: hashedPassword, + first_name: first_name || null, + last_name: last_name || null, + role: 'staff', + permissions: validPermissions, + createdBy: req.admin._id + }) + + res.status(201).json({ admin: sanitizeAdmin(admin) }) + } catch (error) { + next(error) + } +} + +const updateAdminPermissions = async (req, res, next) => { + try { + const { id } = req.params + const { permissions } = req.body + + const admin = await AdminModel.findById(id) + if (!admin) return res.status(404).json({ error: true, message: 'کاربر پیدا نشد' }) + if (admin.role === 'super_admin') { + return res.status(400).json({ error: true, message: 'دسترسی سوپرادمین قابل تغییر نیست' }) + } + + const validPermissions = Array.isArray(permissions) + ? permissions.filter((p) => PANEL_PERMISSION_KEYS.has(p)) + : [] + + admin.permissions = validPermissions + await admin.save() + + res.status(200).json({ admin: sanitizeAdmin(admin) }) + } catch (error) { + next(error) + } +} + +const deleteAdmin = async (req, res, next) => { + try { + const { id } = req.params + const admin = await AdminModel.findById(id) + if (!admin) return res.status(404).json({ error: true, message: 'کاربر پیدا نشد' }) + if (admin.role === 'super_admin') { + return res.status(400).json({ error: true, message: 'حذف سوپرادمین مجاز نیست' }) + } + if (String(admin._id) === String(req.admin._id)) { + return res.status(400).json({ error: true, message: 'نمی‌توانید خودتان را حذف کنید' }) + } + + await AdminModel.deleteOne({ _id: id }) + res.status(200).json({ message: 'کاربر پنل حذف شد' }) + } catch (error) { + next(error) + } +} + +module.exports = { listAdmins, createAdmin, updateAdminPermissions, deleteAdmin } diff --git a/controllers/panel/analytics/analyticsPanelController.js b/controllers/panel/analytics/analyticsPanelController.js new file mode 100644 index 0000000..b4652ec --- /dev/null +++ b/controllers/panel/analytics/analyticsPanelController.js @@ -0,0 +1,247 @@ +const UserModel = require('../../../models/UserModel') +const AnalyticsEvent = require('../../../models/AnalyticsEventModel') + +function parseDays(req) { + const days = parseInt(req.query.days, 10) + if (!days || days <= 0) return 30 + return Math.min(days, 365) +} + +// آمار جغرافیایی/جنسیت مستقیم از خودِ پروفایل کاربرا میاد (چیزی که موقع ثبت‌نام/ویرایش +// پروفایل قبلاً ذخیره می‌شه) — نیازی به ردیابی جداگانه نداره +async function getDemographics(req, res, next) { + try { + const [countries, provinces, cities, genders] = await Promise.all([ + UserModel.aggregate([ + { $match: { 'country.name': { $exists: true, $ne: null } } }, + { $group: { _id: '$country.name', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 20 } + ]), + UserModel.aggregate([ + { $match: { 'province.name': { $exists: true, $ne: null } } }, + { $group: { _id: '$province.name', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 20 } + ]), + UserModel.aggregate([ + { $match: { 'city.name': { $exists: true, $ne: null } } }, + { $group: { _id: '$city.name', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 20 } + ]), + UserModel.aggregate([ + { $group: { _id: '$gender', count: { $sum: 1 } } } + ]) + ]) + + const genderMap = { male: 0, female: 0, unspecified: 0 } + genders.forEach((g) => { + if (g._id === 'male') genderMap.male = g.count + else if (g._id === 'female') genderMap.female = g.count + else genderMap.unspecified += g.count + }) + + res.status(200).json({ + countries: countries.map((c) => ({ name: c._id, count: c.count })), + provinces: provinces.map((p) => ({ name: p._id, count: p.count })), + cities: cities.map((c) => ({ name: c._id, count: c.count })), + gender: genderMap + }) + } catch (error) { + next(error) + } +} + +// دستگاه/مرورگر/سیستم‌عامل/پلتفرم — از رویدادهای خام AnalyticsEvent (فقط بازه‌ی +// اخیر، برای پرفورمنس) +async function getDeviceStats(req, res, next) { + try { + const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000) + const match = { createdAt: { $gte: since } } + + const [devices, browsers, os, platforms] = await Promise.all([ + AnalyticsEvent.aggregate([ + { $match: { ...match, device: { $ne: null } } }, + { $group: { _id: '$device', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 15 } + ]), + AnalyticsEvent.aggregate([ + { $match: { ...match, browser: { $ne: null } } }, + { $group: { _id: '$browser', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 15 } + ]), + AnalyticsEvent.aggregate([ + { $match: { ...match, os: { $ne: null } } }, + { $group: { _id: '$os', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 15 } + ]), + AnalyticsEvent.aggregate([ + { $match: { ...match, type: 'session_start' } }, + { $group: { _id: '$platform', count: { $sum: 1 } } } + ]) + ]) + + res.status(200).json({ + devices: devices.map((d) => ({ name: d._id, count: d.count })), + browsers: browsers.map((b) => ({ name: b._id, count: b.count })), + os: os.map((o) => ({ name: o._id, count: o.count })), + platforms: platforms.reduce( + (acc, p) => ({ ...acc, [p._id]: p.count }), + { web: 0, android: 0, ios: 0 } + ) + }) + } catch (error) { + next(error) + } +} + +// پربازدیدترین صفحات + میانگین زمان موندگاری روی هرکدوم +async function getPageStats(req, res, next) { + try { + const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000) + + const pages = await AnalyticsEvent.aggregate([ + { $match: { type: 'page_view', createdAt: { $gte: since }, path: { $ne: null } } }, + { + $group: { + _id: '$path', + views: { $sum: 1 }, + avgDurationMs: { $avg: '$durationMs' } + } + }, + { $sort: { views: -1 } }, + { $limit: 30 } + ]) + + res.status(200).json({ + pages: pages.map((p) => ({ + path: p._id, + views: p.views, + avgDurationMs: Math.round(p.avgDurationMs || 0) + })) + }) + } catch (error) { + next(error) + } +} + +// پرکلیک‌ترین عناصر (label ارسال‌شده از کلاینت — data-analytics-id یا متن دکمه) +async function getClickStats(req, res, next) { + try { + const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000) + + const clicks = await AnalyticsEvent.aggregate([ + { $match: { type: 'click', createdAt: { $gte: since }, label: { $ne: null } } }, + { $group: { _id: { label: '$label', path: '$path' }, count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 30 } + ]) + + res.status(200).json({ + clicks: clicks.map((c) => ({ + label: c._id.label, + path: c._id.path, + count: c.count + })) + }) + } catch (error) { + next(error) + } +} + +// نرخ بازگشت — بر اساس last_online کاربر نسبت به createdAt؛ کاربری که last_online‌ش +// خیلی نزدیک به createdAt‌شه (یا اصلاً last_online نداره) یعنی بعد از ثبت‌نام دیگه +// برنگشته +async function getRetentionStats(req, res, next) { + try { + const now = Date.now() + const day7 = new Date(now - 7 * 24 * 60 * 60 * 1000) + const day30 = new Date(now - 30 * 24 * 60 * 60 * 1000) + + const [totalUsers, activeLast7Days, activeLast30Days, neverReturned] = + await Promise.all([ + UserModel.countDocuments({}), + UserModel.countDocuments({ last_online: { $gte: day7 } }), + UserModel.countDocuments({ last_online: { $gte: day30 } }), + UserModel.countDocuments({ + $or: [ + { last_online: null }, + { $expr: { $lte: [{ $subtract: ['$last_online', '$createdAt'] }, 60000] } } + ] + }) + ]) + + res.status(200).json({ + totalUsers, + activeLast7Days, + activeLast30Days, + neverReturned, + returningRate: + totalUsers > 0 + ? Math.round(((totalUsers - neverReturned) / totalUsers) * 100) + : 0 + }) + } catch (error) { + next(error) + } +} + +// خلاصه‌ی کلی نشست‌ها — تعداد نشست، میانگین صفحاتِ هر نشست، میانگین زمانِ هر نشست +// (مجموع durationMsِ page_viewهای همون sessionId) +async function getSessionOverview(req, res, next) { + try { + const since = new Date(Date.now() - parseDays(req) * 24 * 60 * 60 * 1000) + + const [sessionCountAgg, sessionDurationAgg] = await Promise.all([ + AnalyticsEvent.aggregate([ + { $match: { type: 'session_start', createdAt: { $gte: since } } }, + { $group: { _id: null, count: { $sum: 1 } } } + ]), + AnalyticsEvent.aggregate([ + { + $match: { + type: 'page_view', + createdAt: { $gte: since }, + durationMs: { $ne: null } + } + }, + { + $group: { + _id: '$sessionId', + pages: { $sum: 1 }, + totalDurationMs: { $sum: '$durationMs' } + } + }, + { + $group: { + _id: null, + avgPagesPerSession: { $avg: '$pages' }, + avgSessionDurationMs: { $avg: '$totalDurationMs' }, + sessionsWithDuration: { $sum: 1 } + } + } + ]) + ]) + + res.status(200).json({ + totalSessions: sessionCountAgg[0]?.count || 0, + avgPagesPerSession: Math.round(sessionDurationAgg[0]?.avgPagesPerSession || 0), + avgSessionDurationMs: Math.round(sessionDurationAgg[0]?.avgSessionDurationMs || 0) + }) + } catch (error) { + next(error) + } +} + +module.exports = { + getDemographics, + getDeviceStats, + getPageStats, + getClickStats, + getRetentionStats, + getSessionOverview +} diff --git a/controllers/panel/auditLog/auditLogController.js b/controllers/panel/auditLog/auditLogController.js new file mode 100644 index 0000000..75371af --- /dev/null +++ b/controllers/panel/auditLog/auditLogController.js @@ -0,0 +1,39 @@ +const AuditLog = require('../../../models/AuditLogModel') +const AdminModel = require('../../../models/AdminModel') + +const listAuditLog = async (req, res, next) => { + try { + const page = Math.max(parseInt(req.query.page, 10) || 1, 1) + const limit = Math.min(parseInt(req.query.limit, 10) || 30, 100) + const filter = {} + if (req.query.adminId) filter.adminId = req.query.adminId + + const [logs, total] = await Promise.all([ + AuditLog.find(filter) + .sort({ createdAt: -1 }) + .skip((page - 1) * limit) + .limit(limit) + .lean(), + AuditLog.countDocuments(filter) + ]) + + res.status(200).json({ + logs, + totalPages: Math.ceil(total / limit) || 1, + totalItems: total + }) + } catch (error) { + next(error) + } +} + +const listAdminsForFilter = async (req, res, next) => { + try { + const admins = await AdminModel.find({}).select('user_name first_name last_name').lean() + res.status(200).json({ admins }) + } catch (error) { + next(error) + } +} + +module.exports = { listAuditLog, listAdminsForFilter } diff --git a/controllers/panel/login/loginController.js b/controllers/panel/login/loginController.js index 1b38717..b045cb0 100644 --- a/controllers/panel/login/loginController.js +++ b/controllers/panel/login/loginController.js @@ -42,7 +42,9 @@ const adminLogin = async (req, res, next) => { return res.json({ message: 'کد تایید صحیح بود', token, - id: user._id + id: user._id, + role: user.role || 'super_admin', + permissions: user.permissions || [] }) } catch (error) { next(error) diff --git a/controllers/panel/moderation/overallStatsController.js b/controllers/panel/moderation/overallStatsController.js new file mode 100644 index 0000000..9b54a59 --- /dev/null +++ b/controllers/panel/moderation/overallStatsController.js @@ -0,0 +1,73 @@ +const UserModel = require('../../../models/UserModel') +const PostModel = require('../../../models/PostModel') +const CommentModel = require('../../../models/CommentModel') +const AdvertisingCommentModel = require('../../../models/AdvertisingCommentModel') +const CourseCommentModel = require('../../../models/CourseComentModel') +const AdvertisingModel = require('../../../models/AdvertisingModel') +const ProjectModel = require('../../../models/ProjectModel') +const CourseModel = require('../../../models/CourseModel') +const AcademyContentModel = require('../../../models/AcademyContentModel') +const ShopModel = require('../../../models/ShopModel') +const ShopProductListingModel = require('../../../models/ShopProductListingModel') +const TicketModel = require('../../../models/TicketModel') +const StoryModel = require('../../../models/StoryModel') + +// عین moderationStatsController ولی برخلاف اون (که فقط موارد در انتظار/تایید موقت رو +// می‌شمره)، اینجا مجموع کلِ هر مدل از ابتدای راه‌اندازی سایته — برای بخش «آمار کلی» +// بالای داشبورد +const getOverallStats = async (req, res, next) => { + try { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + + const [ + users, + posts, + comments, + vitrineComments, + academyComments, + billboards, + projects, + courses, + academyVideos, + shops, + shopProducts, + tickets, + stories, + ] = await Promise.all([ + UserModel.countDocuments({}), + PostModel.countDocuments({}), + CommentModel.countDocuments({}), + AdvertisingCommentModel.countDocuments({}), + CourseCommentModel.countDocuments({}), + AdvertisingModel.countDocuments({}), + ProjectModel.countDocuments({}), + CourseModel.countDocuments({}), + AcademyContentModel.countDocuments({}), + ShopModel.countDocuments({}), + ShopProductListingModel.countDocuments({}), + TicketModel.countDocuments({}), + StoryModel.countDocuments({}), + ]) + + res.status(200).json({ + users, + posts, + comments, + vitrineComments, + academyComments, + billboards, + projects, + courses, + academyVideos, + shops, + shopProducts, + tickets, + stories, + }) + } catch (error) { + next(error) + } +} + +module.exports = { getOverallStats } diff --git a/data/geo/cities.json b/data/geo/cities.json index 69e313e..a4b00fb 100644 --- a/data/geo/cities.json +++ b/data/geo/cities.json @@ -4239,35 +4239,40 @@ "province_id": 128, "country_id": 103, "name": "Asara", - "slug": "ir-alborz-asara" + "slug": "ir-alborz-asara", + "name_fa": "اسارا" }, { "id": 607, "province_id": 128, "country_id": 103, "name": "Charbagh", - "slug": "ir-alborz-charbagh" + "slug": "ir-alborz-charbagh", + "name_fa": "چهارباغ" }, { "id": 608, "province_id": 128, "country_id": 103, "name": "Eshtehard", - "slug": "ir-alborz-eshtehard" + "slug": "ir-alborz-eshtehard", + "name_fa": "اشتهارد" }, { "id": 609, "province_id": 128, "country_id": 103, "name": "Fardis", - "slug": "ir-alborz-fardis" + "slug": "ir-alborz-fardis", + "name_fa": "فردیس" }, { "id": 610, "province_id": 128, "country_id": 103, "name": "Garmdareh", - "slug": "ir-alborz-garmdareh" + "slug": "ir-alborz-garmdareh", + "name_fa": "گرمدره" }, { "id": 611, @@ -4281,7 +4286,8 @@ "province_id": 128, "country_id": 103, "name": "Hashtgerd", - "slug": "ir-alborz-hashtgerd" + "slug": "ir-alborz-hashtgerd", + "name_fa": "هشتگرد" }, { "id": 613, @@ -4295,7 +4301,8 @@ "province_id": 128, "country_id": 103, "name": "Kamalshahr", - "slug": "ir-alborz-kamalshahr" + "slug": "ir-alborz-kamalshahr", + "name_fa": "کمال شهر" }, { "id": 615, @@ -4309,14 +4316,16 @@ "province_id": 128, "country_id": 103, "name": "Koohsar", - "slug": "ir-alborz-koohsar" + "slug": "ir-alborz-koohsar", + "name_fa": "کوهسار" }, { "id": 617, "province_id": 128, "country_id": 103, "name": "Mahdasht", - "slug": "ir-alborz-mahdasht" + "slug": "ir-alborz-mahdasht", + "name_fa": "ماهدشت" }, { "id": 618, @@ -4330,63 +4339,72 @@ "province_id": 128, "country_id": 103, "name": "Nazarabad", - "slug": "ir-alborz-nazarabad" + "slug": "ir-alborz-nazarabad", + "name_fa": "نظرآباد" }, { "id": 620, "province_id": 128, "country_id": 103, "name": "Savojbolagh", - "slug": "ir-alborz-savojbolagh" + "slug": "ir-alborz-savojbolagh", + "name_fa": "ساوجبلاغ" }, { "id": 621, "province_id": 128, "country_id": 103, "name": "Taleqan", - "slug": "ir-alborz-taleqan" + "slug": "ir-alborz-taleqan", + "name_fa": "طالقان" }, { "id": 622, "province_id": 128, "country_id": 103, "name": "Tankaman", - "slug": "ir-alborz-tankaman" + "slug": "ir-alborz-tankaman", + "name_fa": "تنکمان" }, { "id": 623, "province_id": 129, "country_id": 103, "name": "Abibeiglou", - "slug": "ir-ardabil-abibeiglou" + "slug": "ir-ardabil-abibeiglou", + "name_fa": "آبی بیگلو" }, { "id": 624, "province_id": 129, "country_id": 103, "name": "Anbaran", - "slug": "ir-ardabil-anbaran" + "slug": "ir-ardabil-anbaran", + "name_fa": "عنبران" }, { "id": 625, "province_id": 129, "country_id": 103, "name": "Ardabil", - "slug": "ir-ardabil-ardabil" + "slug": "ir-ardabil-ardabil", + "name_fa": "اردبیل" }, { "id": 626, "province_id": 129, "country_id": 103, "name": "Aslan Duz", - "slug": "ir-ardabil-aslan-duz" + "slug": "ir-ardabil-aslan-duz", + "name_fa": "اصلاندوز" }, { "id": 627, "province_id": 129, "country_id": 103, "name": "Bileh Savar", - "slug": "ir-ardabil-bileh-savar" + "slug": "ir-ardabil-bileh-savar", + "name_fa": "بیله سوار" }, { "id": 628, @@ -4400,42 +4418,48 @@ "province_id": 129, "country_id": 103, "name": "Fakhrabad", - "slug": "ir-ardabil-fakhrabad" + "slug": "ir-ardabil-fakhrabad", + "name_fa": "فخرآباد" }, { "id": 630, "province_id": 129, "country_id": 103, "name": "Germi", - "slug": "ir-ardabil-germi" + "slug": "ir-ardabil-germi", + "name_fa": "گرمی" }, { "id": 631, "province_id": 129, "country_id": 103, "name": "Hashjin", - "slug": "ir-ardabil-hashjin" + "slug": "ir-ardabil-hashjin", + "name_fa": "هشتجین" }, { "id": 632, "province_id": 129, "country_id": 103, "name": "Hir", - "slug": "ir-ardabil-hir" + "slug": "ir-ardabil-hir", + "name_fa": "هیر" }, { "id": 633, "province_id": 129, "country_id": 103, "name": "Jafarabad", - "slug": "ir-ardabil-jafarabad" + "slug": "ir-ardabil-jafarabad", + "name_fa": "جعفرآباد" }, { "id": 634, "province_id": 129, "country_id": 103, "name": "Khalkhal", - "slug": "ir-ardabil-khalkhal" + "slug": "ir-ardabil-khalkhal", + "name_fa": "خلخال" }, { "id": 635, @@ -4449,7 +4473,8 @@ "province_id": 129, "country_id": 103, "name": "Kolor", - "slug": "ir-ardabil-kolor" + "slug": "ir-ardabil-kolor", + "name_fa": "کلور" }, { "id": 637, @@ -4463,21 +4488,24 @@ "province_id": 129, "country_id": 103, "name": "Kuraim", - "slug": "ir-ardabil-kuraim" + "slug": "ir-ardabil-kuraim", + "name_fa": "کوراییم" }, { "id": 639, "province_id": 129, "country_id": 103, "name": "Lahroud", - "slug": "ir-ardabil-lahroud" + "slug": "ir-ardabil-lahroud", + "name_fa": "لاهرود" }, { "id": 640, "province_id": 129, "country_id": 103, "name": "Meshgin Shahr", - "slug": "ir-ardabil-meshgin-shahr" + "slug": "ir-ardabil-meshgin-shahr", + "name_fa": "مشگین شهر" }, { "id": 641, @@ -4491,14 +4519,16 @@ "province_id": 129, "country_id": 103, "name": "Namin", - "slug": "ir-ardabil-namin" + "slug": "ir-ardabil-namin", + "name_fa": "نمین" }, { "id": 643, "province_id": 129, "country_id": 103, "name": "Nir", - "slug": "ir-ardabil-nir" + "slug": "ir-ardabil-nir", + "name_fa": "نیر" }, { "id": 644, @@ -4512,7 +4542,8 @@ "province_id": 129, "country_id": 103, "name": "Parsabad", - "slug": "ir-ardabil-parsabad" + "slug": "ir-ardabil-parsabad", + "name_fa": "پارس آباد" }, { "id": 646, @@ -4526,14 +4557,16 @@ "province_id": 129, "country_id": 103, "name": "Razi", - "slug": "ir-ardabil-razi" + "slug": "ir-ardabil-razi", + "name_fa": "رضی" }, { "id": 648, "province_id": 129, "country_id": 103, "name": "Sarein", - "slug": "ir-ardabil-sarein" + "slug": "ir-ardabil-sarein", + "name_fa": "سرعین" }, { "id": 649, @@ -4547,14 +4580,16 @@ "province_id": 129, "country_id": 103, "name": "Tazakand", - "slug": "ir-ardabil-tazakand" + "slug": "ir-ardabil-tazakand", + "name_fa": "تازه کند" }, { "id": 651, "province_id": 130, "country_id": 103, "name": "Ab Pakhsh", - "slug": "ir-bushehr-ab-pakhsh" + "slug": "ir-bushehr-ab-pakhsh", + "name_fa": "آب پخش" }, { "id": 652, @@ -4568,7 +4603,8 @@ "province_id": 130, "country_id": 103, "name": "Ahram", - "slug": "ir-bushehr-ahram" + "slug": "ir-bushehr-ahram", + "name_fa": "اهرم" }, { "id": 654, @@ -4582,7 +4618,8 @@ "province_id": 130, "country_id": 103, "name": "Anarestan", - "slug": "ir-bushehr-anarestan" + "slug": "ir-bushehr-anarestan", + "name_fa": "انارستان" }, { "id": 656, @@ -4631,14 +4668,16 @@ "province_id": 130, "country_id": 103, "name": "Bandar Rig", - "slug": "ir-bushehr-bandar-rig" + "slug": "ir-bushehr-bandar-rig", + "name_fa": "بندرریگ" }, { "id": 663, "province_id": 130, "country_id": 103, "name": "Bandar-e Kangan", - "slug": "ir-bushehr-bandar-e-kangan" + "slug": "ir-bushehr-bandar-e-kangan", + "name_fa": "بندرکنگان" }, { "id": 664, @@ -4659,14 +4698,16 @@ "province_id": 130, "country_id": 103, "name": "Borazjan", - "slug": "ir-bushehr-borazjan" + "slug": "ir-bushehr-borazjan", + "name_fa": "برازجان" }, { "id": 667, "province_id": 130, "country_id": 103, "name": "Bord Khun", - "slug": "ir-bushehr-bord-khun" + "slug": "ir-bushehr-bord-khun", + "name_fa": "بردخون" }, { "id": 668, @@ -4680,21 +4721,24 @@ "province_id": 130, "country_id": 103, "name": "Bushehr", - "slug": "ir-bushehr-bushehr" + "slug": "ir-bushehr-bushehr", + "name_fa": "بوشهر" }, { "id": 670, "province_id": 130, "country_id": 103, "name": "Choghadak", - "slug": "ir-bushehr-choghadak" + "slug": "ir-bushehr-choghadak", + "name_fa": "چغادک" }, { "id": 671, "province_id": 130, "country_id": 103, "name": "Dalaki", - "slug": "ir-bushehr-dalaki" + "slug": "ir-bushehr-dalaki", + "name_fa": "دالکی" }, { "id": 672, @@ -4722,7 +4766,8 @@ "province_id": 130, "country_id": 103, "name": "Delvar", - "slug": "ir-bushehr-delvar" + "slug": "ir-bushehr-delvar", + "name_fa": "دلوار" }, { "id": 676, @@ -4757,14 +4802,16 @@ "province_id": 130, "country_id": 103, "name": "Jam", - "slug": "ir-bushehr-jam" + "slug": "ir-bushehr-jam", + "name_fa": "جم" }, { "id": 681, "province_id": 130, "country_id": 103, "name": "Kaki", - "slug": "ir-bushehr-kaki" + "slug": "ir-bushehr-kaki", + "name_fa": "کاکی" }, { "id": 682, @@ -4792,14 +4839,16 @@ "province_id": 130, "country_id": 103, "name": "Khark", - "slug": "ir-bushehr-khark" + "slug": "ir-bushehr-khark", + "name_fa": "خارک" }, { "id": 686, "province_id": 130, "country_id": 103, "name": "Khormoj", - "slug": "ir-bushehr-khormoj" + "slug": "ir-bushehr-khormoj", + "name_fa": "خورموج" }, { "id": 687, @@ -4813,42 +4862,48 @@ "province_id": 130, "country_id": 103, "name": "Nakhl Taghi", - "slug": "ir-bushehr-nakhl-taghi" + "slug": "ir-bushehr-nakhl-taghi", + "name_fa": "نخل تقی" }, { "id": 689, "province_id": 130, "country_id": 103, "name": "Riz", - "slug": "ir-bushehr-riz" + "slug": "ir-bushehr-riz", + "name_fa": "ریز" }, { "id": 690, "province_id": 130, "country_id": 103, "name": "Sadabad", - "slug": "ir-bushehr-sadabad" + "slug": "ir-bushehr-sadabad", + "name_fa": "سعدآباد" }, { "id": 691, "province_id": 130, "country_id": 103, "name": "Shaban Kareh", - "slug": "ir-bushehr-shaban-kareh" + "slug": "ir-bushehr-shaban-kareh", + "name_fa": "شبانکاره" }, { "id": 692, "province_id": 130, "country_id": 103, "name": "Shonbeh", - "slug": "ir-bushehr-shonbeh" + "slug": "ir-bushehr-shonbeh", + "name_fa": "شنبه" }, { "id": 693, "province_id": 130, "country_id": 103, "name": "Tang Eram", - "slug": "ir-bushehr-tang-eram" + "slug": "ir-bushehr-tang-eram", + "name_fa": "تنگ ارم" }, { "id": 694, @@ -4869,21 +4924,24 @@ "province_id": 131, "country_id": 103, "name": "Aluni", - "slug": "ir-chaharmahal-and-bakhtiari-aluni" + "slug": "ir-chaharmahal-and-bakhtiari-aluni", + "name_fa": "آلونی" }, { "id": 697, "province_id": 131, "country_id": 103, "name": "Ardal", - "slug": "ir-chaharmahal-and-bakhtiari-ardal" + "slug": "ir-chaharmahal-and-bakhtiari-ardal", + "name_fa": "اردل" }, { "id": 698, "province_id": 131, "country_id": 103, "name": "Babaheydar", - "slug": "ir-chaharmahal-and-bakhtiari-babaheydar" + "slug": "ir-chaharmahal-and-bakhtiari-babaheydar", + "name_fa": "باباحیدر" }, { "id": 699, @@ -4897,21 +4955,24 @@ "province_id": 131, "country_id": 103, "name": "Ben", - "slug": "ir-chaharmahal-and-bakhtiari-ben" + "slug": "ir-chaharmahal-and-bakhtiari-ben", + "name_fa": "بن" }, { "id": 701, "province_id": 131, "country_id": 103, "name": "Boldaji", - "slug": "ir-chaharmahal-and-bakhtiari-boldaji" + "slug": "ir-chaharmahal-and-bakhtiari-boldaji", + "name_fa": "بلداجی" }, { "id": 702, "province_id": 131, "country_id": 103, "name": "Boroujen", - "slug": "ir-chaharmahal-and-bakhtiari-boroujen" + "slug": "ir-chaharmahal-and-bakhtiari-boroujen", + "name_fa": "بروجن" }, { "id": 703, @@ -4925,7 +4986,8 @@ "province_id": 131, "country_id": 103, "name": "Chelgard", - "slug": "ir-chaharmahal-and-bakhtiari-chelgard" + "slug": "ir-chaharmahal-and-bakhtiari-chelgard", + "name_fa": "چلگرد" }, { "id": 705, @@ -4960,21 +5022,24 @@ "province_id": 131, "country_id": 103, "name": "Faradonbeh", - "slug": "ir-chaharmahal-and-bakhtiari-faradonbeh" + "slug": "ir-chaharmahal-and-bakhtiari-faradonbeh", + "name_fa": "فرادنبه" }, { "id": 710, "province_id": 131, "country_id": 103, "name": "Farrokh Shahr", - "slug": "ir-chaharmahal-and-bakhtiari-farrokh-shahr" + "slug": "ir-chaharmahal-and-bakhtiari-farrokh-shahr", + "name_fa": "فرخ شهر" }, { "id": 711, "province_id": 131, "country_id": 103, "name": "Farsan", - "slug": "ir-chaharmahal-and-bakhtiari-farsan" + "slug": "ir-chaharmahal-and-bakhtiari-farsan", + "name_fa": "فارسان" }, { "id": 712, @@ -4988,7 +5053,8 @@ "province_id": 131, "country_id": 103, "name": "Gandoman", - "slug": "ir-chaharmahal-and-bakhtiari-gandoman" + "slug": "ir-chaharmahal-and-bakhtiari-gandoman", + "name_fa": "گندمان" }, { "id": 714, @@ -5009,7 +5075,8 @@ "province_id": 131, "country_id": 103, "name": "Hafshejan", - "slug": "ir-chaharmahal-and-bakhtiari-hafshejan" + "slug": "ir-chaharmahal-and-bakhtiari-hafshejan", + "name_fa": "هفشجان" }, { "id": 717, @@ -5037,7 +5104,8 @@ "province_id": 131, "country_id": 103, "name": "Kiar", - "slug": "ir-chaharmahal-and-bakhtiari-kiar" + "slug": "ir-chaharmahal-and-bakhtiari-kiar", + "name_fa": "کیان" }, { "id": 721, @@ -5051,14 +5119,16 @@ "province_id": 131, "country_id": 103, "name": "Lordegan", - "slug": "ir-chaharmahal-and-bakhtiari-lordegan" + "slug": "ir-chaharmahal-and-bakhtiari-lordegan", + "name_fa": "لردگان" }, { "id": 723, "province_id": 131, "country_id": 103, "name": "Mal-e Khalifeh", - "slug": "ir-chaharmahal-and-bakhtiari-mal-e-khalifeh" + "slug": "ir-chaharmahal-and-bakhtiari-mal-e-khalifeh", + "name_fa": "مال خلیفه" }, { "id": 724, @@ -5072,21 +5142,24 @@ "province_id": 131, "country_id": 103, "name": "Nafch", - "slug": "ir-chaharmahal-and-bakhtiari-nafch" + "slug": "ir-chaharmahal-and-bakhtiari-nafch", + "name_fa": "نافچ" }, { "id": 726, "province_id": 131, "country_id": 103, "name": "Naghan", - "slug": "ir-chaharmahal-and-bakhtiari-naghan" + "slug": "ir-chaharmahal-and-bakhtiari-naghan", + "name_fa": "ناغان" }, { "id": 727, "province_id": 131, "country_id": 103, "name": "Naghneh", - "slug": "ir-chaharmahal-and-bakhtiari-naghneh" + "slug": "ir-chaharmahal-and-bakhtiari-naghneh", + "name_fa": "نقنه" }, { "id": 728, @@ -5100,7 +5173,8 @@ "province_id": 131, "country_id": 103, "name": "Saman", - "slug": "ir-chaharmahal-and-bakhtiari-saman" + "slug": "ir-chaharmahal-and-bakhtiari-saman", + "name_fa": "سامان" }, { "id": 730, @@ -5121,14 +5195,16 @@ "province_id": 131, "country_id": 103, "name": "Sefiddasht", - "slug": "ir-chaharmahal-and-bakhtiari-sefiddasht" + "slug": "ir-chaharmahal-and-bakhtiari-sefiddasht", + "name_fa": "سفیددشت" }, { "id": 733, "province_id": 131, "country_id": 103, "name": "Shahrekord", - "slug": "ir-chaharmahal-and-bakhtiari-shahrekord" + "slug": "ir-chaharmahal-and-bakhtiari-shahrekord", + "name_fa": "شهرکرد" }, { "id": 734, @@ -5142,21 +5218,24 @@ "province_id": 131, "country_id": 103, "name": "Shalamzar", - "slug": "ir-chaharmahal-and-bakhtiari-shalamzar" + "slug": "ir-chaharmahal-and-bakhtiari-shalamzar", + "name_fa": "شلمزار" }, { "id": 736, "province_id": 131, "country_id": 103, "name": "Sudejan", - "slug": "ir-chaharmahal-and-bakhtiari-sudejan" + "slug": "ir-chaharmahal-and-bakhtiari-sudejan", + "name_fa": "سودجان" }, { "id": 737, "province_id": 131, "country_id": 103, "name": "Sureshjan", - "slug": "ir-chaharmahal-and-bakhtiari-sureshjan" + "slug": "ir-chaharmahal-and-bakhtiari-sureshjan", + "name_fa": "سورشجان" }, { "id": 738, @@ -5177,7 +5256,8 @@ "province_id": 132, "country_id": 103, "name": "Abeshahmad", - "slug": "ir-east-azerbaijan-abeshahmad" + "slug": "ir-east-azerbaijan-abeshahmad", + "name_fa": "آبش احمد" }, { "id": 741, @@ -5191,14 +5271,16 @@ "province_id": 132, "country_id": 103, "name": "Ahar", - "slug": "ir-east-azerbaijan-ahar" + "slug": "ir-east-azerbaijan-ahar", + "name_fa": "اهر" }, { "id": 743, "province_id": 132, "country_id": 103, "name": "Ajabshir", - "slug": "ir-east-azerbaijan-ajabshir" + "slug": "ir-east-azerbaijan-ajabshir", + "name_fa": "عجب شیر" }, { "id": 744, @@ -5212,21 +5294,24 @@ "province_id": 132, "country_id": 103, "name": "Azarshahr", - "slug": "ir-east-azerbaijan-azarshahr" + "slug": "ir-east-azerbaijan-azarshahr", + "name_fa": "آذرشهر" }, { "id": 746, "province_id": 132, "country_id": 103, "name": "Bakhshāyesh", - "slug": "ir-east-azerbaijan-bakhsh-yesh" + "slug": "ir-east-azerbaijan-bakhsh-yesh", + "name_fa": "بخشایش" }, { "id": 747, "province_id": 132, "country_id": 103, "name": "Basmenj", - "slug": "ir-east-azerbaijan-basmenj" + "slug": "ir-east-azerbaijan-basmenj", + "name_fa": "باسمنج" }, { "id": 748, @@ -5240,14 +5325,16 @@ "province_id": 132, "country_id": 103, "name": "Bonab", - "slug": "ir-east-azerbaijan-bonab" + "slug": "ir-east-azerbaijan-bonab", + "name_fa": "بناب" }, { "id": 750, "province_id": 132, "country_id": 103, "name": "Bostan Abad", - "slug": "ir-east-azerbaijan-bostan-abad" + "slug": "ir-east-azerbaijan-bostan-abad", + "name_fa": "بستان آباد" }, { "id": 751, @@ -5261,28 +5348,32 @@ "province_id": 132, "country_id": 103, "name": "Duzduzan", - "slug": "ir-east-azerbaijan-duzduzan" + "slug": "ir-east-azerbaijan-duzduzan", + "name_fa": "دوزدوزان" }, { "id": 753, "province_id": 132, "country_id": 103, "name": "Gogan", - "slug": "ir-east-azerbaijan-gogan" + "slug": "ir-east-azerbaijan-gogan", + "name_fa": "گوگان" }, { "id": 754, "province_id": 132, "country_id": 103, "name": "Hadishahr", - "slug": "ir-east-azerbaijan-hadishahr" + "slug": "ir-east-azerbaijan-hadishahr", + "name_fa": "هادی شهر" }, { "id": 755, "province_id": 132, "country_id": 103, "name": "Hashtrood", - "slug": "ir-east-azerbaijan-hashtrood" + "slug": "ir-east-azerbaijan-hashtrood", + "name_fa": "هشترود" }, { "id": 756, @@ -5296,21 +5387,24 @@ "province_id": 132, "country_id": 103, "name": "Heris", - "slug": "ir-east-azerbaijan-heris" + "slug": "ir-east-azerbaijan-heris", + "name_fa": "هریس" }, { "id": 758, "province_id": 132, "country_id": 103, "name": "Hurand", - "slug": "ir-east-azerbaijan-hurand" + "slug": "ir-east-azerbaijan-hurand", + "name_fa": "هوراند" }, { "id": 759, "province_id": 132, "country_id": 103, "name": "Ilkhichi", - "slug": "ir-east-azerbaijan-ilkhichi" + "slug": "ir-east-azerbaijan-ilkhichi", + "name_fa": "ایلخچی" }, { "id": 760, @@ -5324,7 +5418,8 @@ "province_id": 132, "country_id": 103, "name": "Jolfa", - "slug": "ir-east-azerbaijan-jolfa" + "slug": "ir-east-azerbaijan-jolfa", + "name_fa": "جلفا" }, { "id": 762, @@ -5345,28 +5440,32 @@ "province_id": 132, "country_id": 103, "name": "Khaje", - "slug": "ir-east-azerbaijan-khaje" + "slug": "ir-east-azerbaijan-khaje", + "name_fa": "خواجه" }, { "id": 765, "province_id": 132, "country_id": 103, "name": "Khamaneh", - "slug": "ir-east-azerbaijan-khamaneh" + "slug": "ir-east-azerbaijan-khamaneh", + "name_fa": "خامنه" }, { "id": 766, "province_id": 132, "country_id": 103, "name": "Kharajoo", - "slug": "ir-east-azerbaijan-kharajoo" + "slug": "ir-east-azerbaijan-kharajoo", + "name_fa": "خراجو" }, { "id": 767, "province_id": 132, "country_id": 103, "name": "Kharvana", - "slug": "ir-east-azerbaijan-kharvana" + "slug": "ir-east-azerbaijan-kharvana", + "name_fa": "خاروانا" }, { "id": 768, @@ -5380,42 +5479,48 @@ "province_id": 132, "country_id": 103, "name": "Khomarlu", - "slug": "ir-east-azerbaijan-khomarlu" + "slug": "ir-east-azerbaijan-khomarlu", + "name_fa": "خمارلو" }, { "id": 770, "province_id": 132, "country_id": 103, "name": "Khosrowshah", - "slug": "ir-east-azerbaijan-khosrowshah" + "slug": "ir-east-azerbaijan-khosrowshah", + "name_fa": "خسروشهر" }, { "id": 771, "province_id": 132, "country_id": 103, "name": "Koozehkonan", - "slug": "ir-east-azerbaijan-koozehkonan" + "slug": "ir-east-azerbaijan-koozehkonan", + "name_fa": "کوزه کنان" }, { "id": 772, "province_id": 132, "country_id": 103, "name": "Koshksaray", - "slug": "ir-east-azerbaijan-koshksaray" + "slug": "ir-east-azerbaijan-koshksaray", + "name_fa": "کشکسرای" }, { "id": 773, "province_id": 132, "country_id": 103, "name": "Leilan", - "slug": "ir-east-azerbaijan-leilan" + "slug": "ir-east-azerbaijan-leilan", + "name_fa": "لیلان" }, { "id": 774, "province_id": 132, "country_id": 103, "name": "Malekan", - "slug": "ir-east-azerbaijan-malekan" + "slug": "ir-east-azerbaijan-malekan", + "name_fa": "ملکان" }, { "id": 775, @@ -5429,28 +5534,32 @@ "province_id": 132, "country_id": 103, "name": "Maragheh", - "slug": "ir-east-azerbaijan-maragheh" + "slug": "ir-east-azerbaijan-maragheh", + "name_fa": "مراغه" }, { "id": 777, "province_id": 132, "country_id": 103, "name": "Marand", - "slug": "ir-east-azerbaijan-marand" + "slug": "ir-east-azerbaijan-marand", + "name_fa": "مرند" }, { "id": 778, "province_id": 132, "country_id": 103, "name": "Mehraban", - "slug": "ir-east-azerbaijan-mehraban" + "slug": "ir-east-azerbaijan-mehraban", + "name_fa": "مهربان" }, { "id": 779, "province_id": 132, "country_id": 103, "name": "Mianeh", - "slug": "ir-east-azerbaijan-mianeh" + "slug": "ir-east-azerbaijan-mianeh", + "name_fa": "میانه" }, { "id": 780, @@ -5464,7 +5573,8 @@ "province_id": 132, "country_id": 103, "name": "Nazarkahrizi", - "slug": "ir-east-azerbaijan-nazarkahrizi" + "slug": "ir-east-azerbaijan-nazarkahrizi", + "name_fa": "نظرکهریزی" }, { "id": 782, @@ -5478,91 +5588,104 @@ "province_id": 132, "country_id": 103, "name": "Qareh Aghaj", - "slug": "ir-east-azerbaijan-qareh-aghaj" + "slug": "ir-east-azerbaijan-qareh-aghaj", + "name_fa": "قره آغاج" }, { "id": 784, "province_id": 132, "country_id": 103, "name": "Sahand", - "slug": "ir-east-azerbaijan-sahand" + "slug": "ir-east-azerbaijan-sahand", + "name_fa": "سهند" }, { "id": 785, "province_id": 132, "country_id": 103, "name": "Sarab", - "slug": "ir-east-azerbaijan-sarab" + "slug": "ir-east-azerbaijan-sarab", + "name_fa": "سراب" }, { "id": 786, "province_id": 132, "country_id": 103, "name": "Sardroud", - "slug": "ir-east-azerbaijan-sardroud" + "slug": "ir-east-azerbaijan-sardroud", + "name_fa": "سردرود" }, { "id": 787, "province_id": 132, "country_id": 103, "name": "Shabestar", - "slug": "ir-east-azerbaijan-shabestar" + "slug": "ir-east-azerbaijan-shabestar", + "name_fa": "شبستر" }, { "id": 788, "province_id": 132, "country_id": 103, "name": "Sharabian", - "slug": "ir-east-azerbaijan-sharabian" + "slug": "ir-east-azerbaijan-sharabian", + "name_fa": "شربیان" }, { "id": 789, "province_id": 132, "country_id": 103, "name": "Sharafkhaneh", - "slug": "ir-east-azerbaijan-sharafkhaneh" + "slug": "ir-east-azerbaijan-sharafkhaneh", + "name_fa": "شرفخانه" }, { "id": 790, "province_id": 132, "country_id": 103, "name": "Shendabad", - "slug": "ir-east-azerbaijan-shendabad" + "slug": "ir-east-azerbaijan-shendabad", + "name_fa": "شندآباد" }, { "id": 791, "province_id": 132, "country_id": 103, "name": "Siahrood", - "slug": "ir-east-azerbaijan-siahrood" + "slug": "ir-east-azerbaijan-siahrood", + "name_fa": "سیه رود" }, { "id": 792, "province_id": 132, "country_id": 103, "name": "Sis", - "slug": "ir-east-azerbaijan-sis" + "slug": "ir-east-azerbaijan-sis", + "name_fa": "سیس" }, { "id": 793, "province_id": 132, "country_id": 103, "name": "Soufian", - "slug": "ir-east-azerbaijan-soufian" + "slug": "ir-east-azerbaijan-soufian", + "name_fa": "صوفیان" }, { "id": 794, "province_id": 132, "country_id": 103, "name": "Tabriz", - "slug": "ir-east-azerbaijan-tabriz" + "slug": "ir-east-azerbaijan-tabriz", + "name_fa": "تبریز" }, { "id": 795, "province_id": 132, "country_id": 103, "name": "Tark", - "slug": "ir-east-azerbaijan-tark" + "slug": "ir-east-azerbaijan-tark", + "name_fa": "ترک" }, { "id": 796, @@ -5576,7 +5699,8 @@ "province_id": 132, "country_id": 103, "name": "Tekmeh Dash", - "slug": "ir-east-azerbaijan-tekmeh-dash" + "slug": "ir-east-azerbaijan-tekmeh-dash", + "name_fa": "تیکمه داش" }, { "id": 798, @@ -5590,14 +5714,16 @@ "province_id": 132, "country_id": 103, "name": "Turkman Chay", - "slug": "ir-east-azerbaijan-turkman-chay" + "slug": "ir-east-azerbaijan-turkman-chay", + "name_fa": "ترکمانچای" }, { "id": 800, "province_id": 132, "country_id": 103, "name": "Vaighan", - "slug": "ir-east-azerbaijan-vaighan" + "slug": "ir-east-azerbaijan-vaighan", + "name_fa": "وایقان" }, { "id": 801, @@ -5618,7 +5744,8 @@ "province_id": 132, "country_id": 103, "name": "Yamchi", - "slug": "ir-east-azerbaijan-yamchi" + "slug": "ir-east-azerbaijan-yamchi", + "name_fa": "یامچی" }, { "id": 804, @@ -5632,28 +5759,32 @@ "province_id": 132, "country_id": 103, "name": "Zonouz", - "slug": "ir-east-azerbaijan-zonouz" + "slug": "ir-east-azerbaijan-zonouz", + "name_fa": "زنوز" }, { "id": 806, "province_id": 133, "country_id": 103, "name": "Abadeh", - "slug": "ir-fars-abadeh" + "slug": "ir-fars-abadeh", + "name_fa": "آباده" }, { "id": 807, "province_id": 133, "country_id": 103, "name": "Abadeh Tashk", - "slug": "ir-fars-abadeh-tashk" + "slug": "ir-fars-abadeh-tashk", + "name_fa": "آباده طشک" }, { "id": 808, "province_id": 133, "country_id": 103, "name": "Ahel", - "slug": "ir-fars-ahel" + "slug": "ir-fars-ahel", + "name_fa": "اهل" }, { "id": 809, @@ -5667,14 +5798,16 @@ "province_id": 133, "country_id": 103, "name": "Alamarvdasht", - "slug": "ir-fars-alamarvdasht" + "slug": "ir-fars-alamarvdasht", + "name_fa": "علامرودشت" }, { "id": 811, "province_id": 133, "country_id": 103, "name": "Arsanjan", - "slug": "ir-fars-arsanjan" + "slug": "ir-fars-arsanjan", + "name_fa": "ارسنجان" }, { "id": 812, @@ -5688,7 +5821,8 @@ "province_id": 133, "country_id": 103, "name": "Ashkanan", - "slug": "ir-fars-ashkanan" + "slug": "ir-fars-ashkanan", + "name_fa": "اشکنان" }, { "id": 814, @@ -5709,21 +5843,24 @@ "province_id": 133, "country_id": 103, "name": "BabAnar", - "slug": "ir-fars-babanar" + "slug": "ir-fars-babanar", + "name_fa": "باب انار" }, { "id": 817, "province_id": 133, "country_id": 103, "name": "Bahman", - "slug": "ir-fars-bahman" + "slug": "ir-fars-bahman", + "name_fa": "بهمن" }, { "id": 818, "province_id": 133, "country_id": 103, "name": "Baladeh", - "slug": "ir-fars-baladeh" + "slug": "ir-fars-baladeh", + "name_fa": "بالاده" }, { "id": 819, @@ -5737,7 +5874,8 @@ "province_id": 133, "country_id": 103, "name": "Bavanat", - "slug": "ir-fars-bavanat" + "slug": "ir-fars-bavanat", + "name_fa": "بوانات" }, { "id": 821, @@ -5765,42 +5903,48 @@ "province_id": 133, "country_id": 103, "name": "Dabiran", - "slug": "ir-fars-dabiran" + "slug": "ir-fars-dabiran", + "name_fa": "دبیران" }, { "id": 825, "province_id": 133, "country_id": 103, "name": "Darab", - "slug": "ir-fars-darab" + "slug": "ir-fars-darab", + "name_fa": "داراب" }, { "id": 826, "province_id": 133, "country_id": 103, "name": "Dariun", - "slug": "ir-fars-dariun" + "slug": "ir-fars-dariun", + "name_fa": "داریان" }, { "id": 827, "province_id": 133, "country_id": 103, "name": "Dehram", - "slug": "ir-fars-dehram" + "slug": "ir-fars-dehram", + "name_fa": "دهرم" }, { "id": 828, "province_id": 133, "country_id": 103, "name": "Dezhkord", - "slug": "ir-fars-dezhkord" + "slug": "ir-fars-dezhkord", + "name_fa": "دژکرد" }, { "id": 829, "province_id": 133, "country_id": 103, "name": "DoBorji", - "slug": "ir-fars-doborji" + "slug": "ir-fars-doborji", + "name_fa": "دوبرجی" }, { "id": 830, @@ -5821,7 +5965,8 @@ "province_id": 133, "country_id": 103, "name": "Eij", - "slug": "ir-fars-eij" + "slug": "ir-fars-eij", + "name_fa": "ایج" }, { "id": 833, @@ -5835,7 +5980,8 @@ "province_id": 133, "country_id": 103, "name": "Emam Shahr", - "slug": "ir-fars-emam-shahr" + "slug": "ir-fars-emam-shahr", + "name_fa": "امام شهر" }, { "id": 835, @@ -5849,7 +5995,8 @@ "province_id": 133, "country_id": 103, "name": "Estahban", - "slug": "ir-fars-estahban" + "slug": "ir-fars-estahban", + "name_fa": "استهبان" }, { "id": 837, @@ -5863,42 +6010,48 @@ "province_id": 133, "country_id": 103, "name": "Fadami", - "slug": "ir-fars-fadami" + "slug": "ir-fars-fadami", + "name_fa": "فدامی" }, { "id": 839, "province_id": 133, "country_id": 103, "name": "Farashband", - "slug": "ir-fars-farashband" + "slug": "ir-fars-farashband", + "name_fa": "فراشبند" }, { "id": 840, "province_id": 133, "country_id": 103, "name": "Fasa", - "slug": "ir-fars-fasa" + "slug": "ir-fars-fasa", + "name_fa": "فسا" }, { "id": 841, "province_id": 133, "country_id": 103, "name": "Firuzabad", - "slug": "ir-fars-firuzabad" + "slug": "ir-fars-firuzabad", + "name_fa": "فیروزآباد" }, { "id": 842, "province_id": 133, "country_id": 103, "name": "Galehdar", - "slug": "ir-fars-galehdar" + "slug": "ir-fars-galehdar", + "name_fa": "گله دار" }, { "id": 843, "province_id": 133, "country_id": 103, "name": "Gerash", - "slug": "ir-fars-gerash" + "slug": "ir-fars-gerash", + "name_fa": "گراش" }, { "id": 844, @@ -5919,7 +6072,8 @@ "province_id": 133, "country_id": 103, "name": "HajiAbad", - "slug": "ir-fars-hajiabad" + "slug": "ir-fars-hajiabad", + "name_fa": "قادرآباد" }, { "id": 847, @@ -5933,7 +6087,8 @@ "province_id": 133, "country_id": 103, "name": "HasanAbad", - "slug": "ir-fars-hasanabad" + "slug": "ir-fars-hasanabad", + "name_fa": "حسن آباد" }, { "id": 849, @@ -5947,21 +6102,24 @@ "province_id": 133, "country_id": 103, "name": "IzadKhast", - "slug": "ir-fars-izadkhast" + "slug": "ir-fars-izadkhast", + "name_fa": "ایزدخواست" }, { "id": 851, "province_id": 133, "country_id": 103, "name": "Jahrom", - "slug": "ir-fars-jahrom" + "slug": "ir-fars-jahrom", + "name_fa": "جهرم" }, { "id": 852, "province_id": 133, "country_id": 103, "name": "JannatShahr", - "slug": "ir-fars-jannatshahr" + "slug": "ir-fars-jannatshahr", + "name_fa": "جنت شهر" }, { "id": 853, @@ -5975,7 +6133,8 @@ "province_id": 133, "country_id": 103, "name": "Kamfiruz", - "slug": "ir-fars-kamfiruz" + "slug": "ir-fars-kamfiruz", + "name_fa": "کامفیروز" }, { "id": 855, @@ -5996,14 +6155,16 @@ "province_id": 133, "country_id": 103, "name": "Kazerun", - "slug": "ir-fars-kazerun" + "slug": "ir-fars-kazerun", + "name_fa": "کازرون" }, { "id": 858, "province_id": 133, "country_id": 103, "name": "KhaneZenian", - "slug": "ir-fars-khanezenian" + "slug": "ir-fars-khanezenian", + "name_fa": "خان زنیان" }, { "id": 859, @@ -6017,14 +6178,16 @@ "province_id": 133, "country_id": 103, "name": "Kharameh", - "slug": "ir-fars-kharameh" + "slug": "ir-fars-kharameh", + "name_fa": "خرامه" }, { "id": 861, "province_id": 133, "country_id": 103, "name": "Khavaran", - "slug": "ir-fars-khavaran" + "slug": "ir-fars-khavaran", + "name_fa": "خاوران" }, { "id": 862, @@ -6038,14 +6201,16 @@ "province_id": 133, "country_id": 103, "name": "Khesht", - "slug": "ir-fars-khesht" + "slug": "ir-fars-khesht", + "name_fa": "خشت" }, { "id": 864, "province_id": 133, "country_id": 103, "name": "Khonj", - "slug": "ir-fars-khonj" + "slug": "ir-fars-khonj", + "name_fa": "خنج" }, { "id": 865, @@ -6073,14 +6238,16 @@ "province_id": 133, "country_id": 103, "name": "Khour", - "slug": "ir-fars-khour" + "slug": "ir-fars-khour", + "name_fa": "خور" }, { "id": 869, "province_id": 133, "country_id": 103, "name": "Konartakhteh", - "slug": "ir-fars-konartakhteh" + "slug": "ir-fars-konartakhteh", + "name_fa": "کنارتخته" }, { "id": 870, @@ -6115,7 +6282,8 @@ "province_id": 133, "country_id": 103, "name": "Lamerd", - "slug": "ir-fars-lamerd" + "slug": "ir-fars-lamerd", + "name_fa": "لامرد" }, { "id": 875, @@ -6129,7 +6297,8 @@ "province_id": 133, "country_id": 103, "name": "Lar", - "slug": "ir-fars-lar" + "slug": "ir-fars-lar", + "name_fa": "لار" }, { "id": 877, @@ -6143,7 +6312,8 @@ "province_id": 133, "country_id": 103, "name": "Latifi", - "slug": "ir-fars-latifi" + "slug": "ir-fars-latifi", + "name_fa": "لطیفی" }, { "id": 879, @@ -6157,14 +6327,16 @@ "province_id": 133, "country_id": 103, "name": "Marvdasht", - "slug": "ir-fars-marvdasht" + "slug": "ir-fars-marvdasht", + "name_fa": "مرودشت" }, { "id": 881, "province_id": 133, "country_id": 103, "name": "Masiri", - "slug": "ir-fars-masiri" + "slug": "ir-fars-masiri", + "name_fa": "مصیری" }, { "id": 882, @@ -6178,7 +6350,8 @@ "province_id": 133, "country_id": 103, "name": "Meshkan", - "slug": "ir-fars-meshkan" + "slug": "ir-fars-meshkan", + "name_fa": "مشکان" }, { "id": 884, @@ -6206,42 +6379,48 @@ "province_id": 133, "country_id": 103, "name": "Mohr", - "slug": "ir-fars-mohr" + "slug": "ir-fars-mohr", + "name_fa": "مهر" }, { "id": 888, "province_id": 133, "country_id": 103, "name": "Neyriz", - "slug": "ir-fars-neyriz" + "slug": "ir-fars-neyriz", + "name_fa": "نی ریز" }, { "id": 889, "province_id": 133, "country_id": 103, "name": "No Bandegan", - "slug": "ir-fars-no-bandegan" + "slug": "ir-fars-no-bandegan", + "name_fa": "نوبندگان" }, { "id": 890, "province_id": 133, "country_id": 103, "name": "Nourabad", - "slug": "ir-fars-nourabad" + "slug": "ir-fars-nourabad", + "name_fa": "نورآباد" }, { "id": 891, "province_id": 133, "country_id": 103, "name": "Nowdan", - "slug": "ir-fars-nowdan" + "slug": "ir-fars-nowdan", + "name_fa": "نودان" }, { "id": 892, "province_id": 133, "country_id": 103, "name": "Nowjein", - "slug": "ir-fars-nowjein" + "slug": "ir-fars-nowjein", + "name_fa": "نوجین" }, { "id": 893, @@ -6297,7 +6476,8 @@ "province_id": 133, "country_id": 103, "name": "QotbAbad", - "slug": "ir-fars-qotbabad" + "slug": "ir-fars-qotbabad", + "name_fa": "قطب آباد" }, { "id": 901, @@ -6311,14 +6491,16 @@ "province_id": 133, "country_id": 103, "name": "Rostam", - "slug": "ir-fars-rostam" + "slug": "ir-fars-rostam", + "name_fa": "سروستان" }, { "id": 903, "province_id": 133, "country_id": 103, "name": "SaadatShahr", - "slug": "ir-fars-saadatshahr" + "slug": "ir-fars-saadatshahr", + "name_fa": "سعادت شهر" }, { "id": 904, @@ -6332,7 +6514,8 @@ "province_id": 133, "country_id": 103, "name": "Safashahr", - "slug": "ir-fars-safashahr" + "slug": "ir-fars-safashahr", + "name_fa": "صفاشهر" }, { "id": 906, @@ -6353,7 +6536,8 @@ "province_id": 133, "country_id": 103, "name": "Sepidan", - "slug": "ir-fars-sepidan" + "slug": "ir-fars-sepidan", + "name_fa": "سیدان" }, { "id": 909, @@ -6367,21 +6551,24 @@ "province_id": 133, "country_id": 103, "name": "Sheshdeh", - "slug": "ir-fars-sheshdeh" + "slug": "ir-fars-sheshdeh", + "name_fa": "ششده" }, { "id": 911, "province_id": 133, "country_id": 103, "name": "Shiraz", - "slug": "ir-fars-shiraz" + "slug": "ir-fars-shiraz", + "name_fa": "شیراز" }, { "id": 912, "province_id": 133, "country_id": 103, "name": "Soghad", - "slug": "ir-fars-soghad" + "slug": "ir-fars-soghad", + "name_fa": "صغاد" }, { "id": 913, @@ -6409,21 +6596,24 @@ "province_id": 133, "country_id": 103, "name": "Zahedshahr", - "slug": "ir-fars-zahedshahr" + "slug": "ir-fars-zahedshahr", + "name_fa": "زاهدشهر" }, { "id": 917, "province_id": 133, "country_id": 103, "name": "Zarghan", - "slug": "ir-fars-zarghan" + "slug": "ir-fars-zarghan", + "name_fa": "زرقان" }, { "id": 918, "province_id": 133, "country_id": 103, "name": "Zarrin Dasht", - "slug": "ir-fars-zarrin-dasht" + "slug": "ir-fars-zarrin-dasht", + "name_fa": "زرین دشت" }, { "id": 919, @@ -6696,7 +6886,8 @@ "province_id": 134, "country_id": 103, "name": "Rahimabad", - "slug": "ir-gilan-rahimabad" + "slug": "ir-gilan-rahimabad", + "name_fa": "خرم آباد" }, { "id": 958, @@ -6815,14 +7006,16 @@ "province_id": 135, "country_id": 103, "name": "Aliabad-e-Katul", - "slug": "ir-golestan-aliabad-e-katul" + "slug": "ir-golestan-aliabad-e-katul", + "name_fa": "علی آباد کتول" }, { "id": 975, "province_id": 135, "country_id": 103, "name": "Anbar Olum", - "slug": "ir-golestan-anbar-olum" + "slug": "ir-golestan-anbar-olum", + "name_fa": "انبارآلوم" }, { "id": 976, @@ -6843,14 +7036,16 @@ "province_id": 135, "country_id": 103, "name": "Azadshahr", - "slug": "ir-golestan-azadshahr" + "slug": "ir-golestan-azadshahr", + "name_fa": "آزادشهر" }, { "id": 979, "province_id": 135, "country_id": 103, "name": "Bandar Torkaman", - "slug": "ir-golestan-bandar-torkaman" + "slug": "ir-golestan-bandar-torkaman", + "name_fa": "بندرترکمن" }, { "id": 980, @@ -6864,7 +7059,8 @@ "province_id": 135, "country_id": 103, "name": "Daland", - "slug": "ir-golestan-daland" + "slug": "ir-golestan-daland", + "name_fa": "دلند" }, { "id": 982, @@ -6878,21 +7074,24 @@ "province_id": 135, "country_id": 103, "name": "Fazelabad", - "slug": "ir-golestan-fazelabad" + "slug": "ir-golestan-fazelabad", + "name_fa": "فاضل آباد" }, { "id": 984, "province_id": 135, "country_id": 103, "name": "Galikesh", - "slug": "ir-golestan-galikesh" + "slug": "ir-golestan-galikesh", + "name_fa": "گالیکش" }, { "id": 985, "province_id": 135, "country_id": 103, "name": "Gomish Tepe", - "slug": "ir-golestan-gomish-tepe" + "slug": "ir-golestan-gomish-tepe", + "name_fa": "گمیش تپه" }, { "id": 986, @@ -6906,42 +7105,48 @@ "province_id": 135, "country_id": 103, "name": "Gonbad Kavus", - "slug": "ir-golestan-gonbad-kavus" + "slug": "ir-golestan-gonbad-kavus", + "name_fa": "گنبدکاووس" }, { "id": 988, "province_id": 135, "country_id": 103, "name": "Gorgan", - "slug": "ir-golestan-gorgan" + "slug": "ir-golestan-gorgan", + "name_fa": "گرگان" }, { "id": 989, "province_id": 135, "country_id": 103, "name": "Incheboron", - "slug": "ir-golestan-incheboron" + "slug": "ir-golestan-incheboron", + "name_fa": "اینچه برون" }, { "id": 990, "province_id": 135, "country_id": 103, "name": "Jelin", - "slug": "ir-golestan-jelin" + "slug": "ir-golestan-jelin", + "name_fa": "جلین" }, { "id": 991, "province_id": 135, "country_id": 103, "name": "Kalaleh", - "slug": "ir-golestan-kalaleh" + "slug": "ir-golestan-kalaleh", + "name_fa": "کلاله" }, { "id": 992, "province_id": 135, "country_id": 103, "name": "Khānbebin", - "slug": "ir-golestan-kh-nbebin" + "slug": "ir-golestan-kh-nbebin", + "name_fa": "خان ببین" }, { "id": 993, @@ -6976,28 +7181,32 @@ "province_id": 135, "country_id": 103, "name": "Minudasht", - "slug": "ir-golestan-minudasht" + "slug": "ir-golestan-minudasht", + "name_fa": "مینودشت" }, { "id": 998, "province_id": 135, "country_id": 103, "name": "Neginshahr", - "slug": "ir-golestan-neginshahr" + "slug": "ir-golestan-neginshahr", + "name_fa": "نگین شهر" }, { "id": 999, "province_id": 135, "country_id": 103, "name": "Nowdeh Khanduz", - "slug": "ir-golestan-nowdeh-khanduz" + "slug": "ir-golestan-nowdeh-khanduz", + "name_fa": "نوده خاندوز" }, { "id": 1000, "province_id": 135, "country_id": 103, "name": "Nowkandeh", - "slug": "ir-golestan-nowkandeh" + "slug": "ir-golestan-nowkandeh", + "name_fa": "نوکنده" }, { "id": 1001, @@ -7011,7 +7220,8 @@ "province_id": 135, "country_id": 103, "name": "Ramian", - "slug": "ir-golestan-ramian" + "slug": "ir-golestan-ramian", + "name_fa": "رامیان" }, { "id": 1003, @@ -7025,14 +7235,16 @@ "province_id": 135, "country_id": 103, "name": "Sarkhon Kalateh", - "slug": "ir-golestan-sarkhon-kalateh" + "slug": "ir-golestan-sarkhon-kalateh", + "name_fa": "سرخنکلاته" }, { "id": 1005, "province_id": 135, "country_id": 103, "name": "Siminshahr", - "slug": "ir-golestan-siminshahr" + "slug": "ir-golestan-siminshahr", + "name_fa": "سیمین شهر" }, { "id": 1006, @@ -7333,7 +7545,8 @@ "province_id": 136, "country_id": 103, "name": "Asadabad", - "slug": "ir-hamadan-asadabad" + "slug": "ir-hamadan-asadabad", + "name_fa": "اسدآباد" }, { "id": 1049, @@ -7354,7 +7567,8 @@ "province_id": 136, "country_id": 103, "name": "Azandarian", - "slug": "ir-hamadan-azandarian" + "slug": "ir-hamadan-azandarian", + "name_fa": "ازندریان" }, { "id": 1052, @@ -7403,7 +7617,8 @@ "province_id": 136, "country_id": 103, "name": "Bahar", - "slug": "ir-hamadan-bahar" + "slug": "ir-hamadan-bahar", + "name_fa": "بهار" }, { "id": 1059, @@ -7445,7 +7660,8 @@ "province_id": 136, "country_id": 103, "name": "Barzul", - "slug": "ir-hamadan-barzul" + "slug": "ir-hamadan-barzul", + "name_fa": "برزول" }, { "id": 1065, @@ -7949,14 +8165,16 @@ "province_id": 136, "country_id": 103, "name": "Famenin", - "slug": "ir-hamadan-famenin" + "slug": "ir-hamadan-famenin", + "name_fa": "فامنین" }, { "id": 1137, "province_id": 136, "country_id": 103, "name": "Farasfaj", - "slug": "ir-hamadan-farasfaj" + "slug": "ir-hamadan-farasfaj", + "name_fa": "فرسفج" }, { "id": 1138, @@ -8005,7 +8223,8 @@ "province_id": 136, "country_id": 103, "name": "Firuzan", - "slug": "ir-hamadan-firuzan" + "slug": "ir-hamadan-firuzan", + "name_fa": "فیروزان" }, { "id": 1145, @@ -8082,7 +8301,8 @@ "province_id": 136, "country_id": 103, "name": "Giyan", - "slug": "ir-hamadan-giyan" + "slug": "ir-hamadan-giyan", + "name_fa": "گیان" }, { "id": 1156, @@ -8243,7 +8463,8 @@ "province_id": 136, "country_id": 103, "name": "Hamadan", - "slug": "ir-hamadan-hamadan" + "slug": "ir-hamadan-hamadan", + "name_fa": "همدان" }, { "id": 1179, @@ -8474,7 +8695,8 @@ "province_id": 136, "country_id": 103, "name": "Jowkar", - "slug": "ir-hamadan-jowkar" + "slug": "ir-hamadan-jowkar", + "name_fa": "جوکار" }, { "id": 1212, @@ -8488,7 +8710,8 @@ "province_id": 136, "country_id": 103, "name": "Kaboodrahang", - "slug": "ir-hamadan-kaboodrahang" + "slug": "ir-hamadan-kaboodrahang", + "name_fa": "کبودر آهنگ" }, { "id": 1214, @@ -8845,7 +9068,8 @@ "province_id": 136, "country_id": 103, "name": "Lalejin", - "slug": "ir-hamadan-lalejin" + "slug": "ir-hamadan-lalejin", + "name_fa": "لالجین" }, { "id": 1265, @@ -8950,7 +9174,8 @@ "province_id": 136, "country_id": 103, "name": "Maryanaj", - "slug": "ir-hamadan-maryanaj" + "slug": "ir-hamadan-maryanaj", + "name_fa": "مریانج" }, { "id": 1280, @@ -9111,7 +9336,8 @@ "province_id": 136, "country_id": 103, "name": "Nahavand", - "slug": "ir-hamadan-nahavand" + "slug": "ir-hamadan-nahavand", + "name_fa": "نهاوند" }, { "id": 1303, @@ -9734,7 +9960,8 @@ "province_id": 136, "country_id": 103, "name": "Salehabad", - "slug": "ir-hamadan-salehabad" + "slug": "ir-hamadan-salehabad", + "name_fa": "صالح آباد" }, { "id": 1392, @@ -9755,7 +9982,8 @@ "province_id": 136, "country_id": 103, "name": "Samen", - "slug": "ir-hamadan-samen" + "slug": "ir-hamadan-samen", + "name_fa": "سامن" }, { "id": 1395, @@ -9811,7 +10039,8 @@ "province_id": 136, "country_id": 103, "name": "Sarkan", - "slug": "ir-hamadan-sarkan" + "slug": "ir-hamadan-sarkan", + "name_fa": "سرکان" }, { "id": 1403, @@ -9930,7 +10159,8 @@ "province_id": 136, "country_id": 103, "name": "Shirin Su", - "slug": "ir-hamadan-shirin-su" + "slug": "ir-hamadan-shirin-su", + "name_fa": "شیرین سو" }, { "id": 1420, @@ -10203,7 +10433,8 @@ "province_id": 136, "country_id": 103, "name": "Tuyserkan", - "slug": "ir-hamadan-tuyserkan" + "slug": "ir-hamadan-tuyserkan", + "name_fa": "تویسرکان" }, { "id": 1459, @@ -10441,7 +10672,8 @@ "province_id": 136, "country_id": 103, "name": "Zangeneh", - "slug": "ir-hamadan-zangeneh" + "slug": "ir-hamadan-zangeneh", + "name_fa": "زنگنه" }, { "id": 1493, @@ -10504,21 +10736,24 @@ "province_id": 137, "country_id": 103, "name": "Bandar Abbas", - "slug": "ir-hormozgan-bandar-abbas" + "slug": "ir-hormozgan-bandar-abbas", + "name_fa": "بندرعباس" }, { "id": 1502, "province_id": 137, "country_id": 103, "name": "Bandar Charak", - "slug": "ir-hormozgan-bandar-charak" + "slug": "ir-hormozgan-bandar-charak", + "name_fa": "بندرچارک" }, { "id": 1503, "province_id": 137, "country_id": 103, "name": "Bandar Khamir", - "slug": "ir-hormozgan-bandar-khamir" + "slug": "ir-hormozgan-bandar-khamir", + "name_fa": "بندرخمیر" }, { "id": 1504, @@ -10532,14 +10767,16 @@ "province_id": 137, "country_id": 103, "name": "Bandar Lengeh", - "slug": "ir-hormozgan-bandar-lengeh" + "slug": "ir-hormozgan-bandar-lengeh", + "name_fa": "بندرلنگه" }, { "id": 1506, "province_id": 137, "country_id": 103, "name": "Bandar-e-Jask", - "slug": "ir-hormozgan-bandar-e-jask" + "slug": "ir-hormozgan-bandar-e-jask", + "name_fa": "بندرجاسک" }, { "id": 1507, @@ -10553,21 +10790,24 @@ "province_id": 137, "country_id": 103, "name": "Bastak", - "slug": "ir-hormozgan-bastak" + "slug": "ir-hormozgan-bastak", + "name_fa": "بستک" }, { "id": 1509, "province_id": 137, "country_id": 103, "name": "Bikah", - "slug": "ir-hormozgan-bikah" + "slug": "ir-hormozgan-bikah", + "name_fa": "بیکا" }, { "id": 1510, "province_id": 137, "country_id": 103, "name": "Dargahan", - "slug": "ir-hormozgan-dargahan" + "slug": "ir-hormozgan-dargahan", + "name_fa": "درگهان" }, { "id": 1511, @@ -10581,21 +10821,24 @@ "province_id": 137, "country_id": 103, "name": "Dehbārez", - "slug": "ir-hormozgan-dehb-rez" + "slug": "ir-hormozgan-dehb-rez", + "name_fa": "دهبارز" }, { "id": 1513, "province_id": 137, "country_id": 103, "name": "Fareghan", - "slug": "ir-hormozgan-fareghan" + "slug": "ir-hormozgan-fareghan", + "name_fa": "فارغان" }, { "id": 1514, "province_id": 137, "country_id": 103, "name": "Fin", - "slug": "ir-hormozgan-fin" + "slug": "ir-hormozgan-fin", + "name_fa": "فین" }, { "id": 1515, @@ -10609,14 +10852,16 @@ "province_id": 137, "country_id": 103, "name": "Gouharan", - "slug": "ir-hormozgan-gouharan" + "slug": "ir-hormozgan-gouharan", + "name_fa": "گوهران" }, { "id": 1517, "province_id": 137, "country_id": 103, "name": "Haji Abad", - "slug": "ir-hormozgan-haji-abad" + "slug": "ir-hormozgan-haji-abad", + "name_fa": "حاجی آباد" }, { "id": 1518, @@ -10630,21 +10875,24 @@ "province_id": 137, "country_id": 103, "name": "Hasht Bandi", - "slug": "ir-hormozgan-hasht-bandi" + "slug": "ir-hormozgan-hasht-bandi", + "name_fa": "هشتبندی" }, { "id": 1520, "province_id": 137, "country_id": 103, "name": "Hormuz", - "slug": "ir-hormozgan-hormuz" + "slug": "ir-hormozgan-hormuz", + "name_fa": "هرمز" }, { "id": 1521, "province_id": 137, "country_id": 103, "name": "Janah", - "slug": "ir-hormozgan-janah" + "slug": "ir-hormozgan-janah", + "name_fa": "جناح" }, { "id": 1522, @@ -10665,14 +10913,16 @@ "province_id": 137, "country_id": 103, "name": "Kish", - "slug": "ir-hormozgan-kish" + "slug": "ir-hormozgan-kish", + "name_fa": "کیش" }, { "id": 1525, "province_id": 137, "country_id": 103, "name": "Koshkonar", - "slug": "ir-hormozgan-koshkonar" + "slug": "ir-hormozgan-koshkonar", + "name_fa": "کوشکنار" }, { "id": 1526, @@ -10693,14 +10943,16 @@ "province_id": 137, "country_id": 103, "name": "Minab", - "slug": "ir-hormozgan-minab" + "slug": "ir-hormozgan-minab", + "name_fa": "میناب" }, { "id": 1529, "province_id": 137, "country_id": 103, "name": "Parsian", - "slug": "ir-hormozgan-parsian" + "slug": "ir-hormozgan-parsian", + "name_fa": "پارسیان" }, { "id": 1530, @@ -10749,28 +11001,32 @@ "province_id": 137, "country_id": 103, "name": "Senderk", - "slug": "ir-hormozgan-senderk" + "slug": "ir-hormozgan-senderk", + "name_fa": "سندرک" }, { "id": 1537, "province_id": 137, "country_id": 103, "name": "Sirik", - "slug": "ir-hormozgan-sirik" + "slug": "ir-hormozgan-sirik", + "name_fa": "سیریک" }, { "id": 1538, "province_id": 137, "country_id": 103, "name": "Suzā", - "slug": "ir-hormozgan-suza" + "slug": "ir-hormozgan-suza", + "name_fa": "سوزا" }, { "id": 1539, "province_id": 137, "country_id": 103, "name": "Takht", - "slug": "ir-hormozgan-takht" + "slug": "ir-hormozgan-takht", + "name_fa": "تخت" }, { "id": 1540, @@ -10791,35 +11047,40 @@ "province_id": 137, "country_id": 103, "name": "Ziyārat Ali", - "slug": "ir-hormozgan-ziy-rat-ali" + "slug": "ir-hormozgan-ziy-rat-ali", + "name_fa": "زیارتعلی" }, { "id": 1543, "province_id": 138, "country_id": 103, "name": "abdanan", - "slug": "ir-ilam-abdanan" + "slug": "ir-ilam-abdanan", + "name_fa": "آبدانان" }, { "id": 1544, "province_id": 138, "country_id": 103, "name": "Arkavaz", - "slug": "ir-ilam-arkavaz" + "slug": "ir-ilam-arkavaz", + "name_fa": "ارکواز" }, { "id": 1545, "province_id": 138, "country_id": 103, "name": "Aseman Abad", - "slug": "ir-ilam-aseman-abad" + "slug": "ir-ilam-aseman-abad", + "name_fa": "آسمان آباد" }, { "id": 1546, "province_id": 138, "country_id": 103, "name": "Badreh", - "slug": "ir-ilam-badreh" + "slug": "ir-ilam-badreh", + "name_fa": "بدره" }, { "id": 1547, @@ -10847,28 +11108,32 @@ "province_id": 138, "country_id": 103, "name": "Chovar", - "slug": "ir-ilam-chovar" + "slug": "ir-ilam-chovar", + "name_fa": "چوار" }, { "id": 1551, "province_id": 138, "country_id": 103, "name": "Darreh Shahr", - "slug": "ir-ilam-darreh-shahr" + "slug": "ir-ilam-darreh-shahr", + "name_fa": "دره شهر" }, { "id": 1552, "province_id": 138, "country_id": 103, "name": "Dehloran", - "slug": "ir-ilam-dehloran" + "slug": "ir-ilam-dehloran", + "name_fa": "دهلران" }, { "id": 1553, "province_id": 138, "country_id": 103, "name": "Delgosha", - "slug": "ir-ilam-delgosha" + "slug": "ir-ilam-delgosha", + "name_fa": "دلگشا" }, { "id": 1554, @@ -10882,14 +11147,16 @@ "province_id": 138, "country_id": 103, "name": "Ilam", - "slug": "ir-ilam-ilam" + "slug": "ir-ilam-ilam", + "name_fa": "ایلام" }, { "id": 1556, "province_id": 138, "country_id": 103, "name": "Loumar", - "slug": "ir-ilam-loumar" + "slug": "ir-ilam-loumar", + "name_fa": "لومار" }, { "id": 1557, @@ -10924,49 +11191,56 @@ "province_id": 138, "country_id": 103, "name": "Mehran", - "slug": "ir-ilam-mehran" + "slug": "ir-ilam-mehran", + "name_fa": "مهران" }, { "id": 1562, "province_id": 138, "country_id": 103, "name": "Mousiyan", - "slug": "ir-ilam-mousiyan" + "slug": "ir-ilam-mousiyan", + "name_fa": "موسیان" }, { "id": 1563, "province_id": 138, "country_id": 103, "name": "Murmuri", - "slug": "ir-ilam-murmuri" + "slug": "ir-ilam-murmuri", + "name_fa": "مورموری" }, { "id": 1564, "province_id": 138, "country_id": 103, "name": "Pahleh", - "slug": "ir-ilam-pahleh" + "slug": "ir-ilam-pahleh", + "name_fa": "پهله" }, { "id": 1565, "province_id": 138, "country_id": 103, "name": "Saleh Abad", - "slug": "ir-ilam-saleh-abad" + "slug": "ir-ilam-saleh-abad", + "name_fa": "صالح آباد" }, { "id": 1566, "province_id": 138, "country_id": 103, "name": "Sarabbagh", - "slug": "ir-ilam-sarabbagh" + "slug": "ir-ilam-sarabbagh", + "name_fa": "سراب باغ" }, { "id": 1567, "province_id": 138, "country_id": 103, "name": "Sarableh", - "slug": "ir-ilam-sarableh" + "slug": "ir-ilam-sarableh", + "name_fa": "سرابله" }, { "id": 1568, @@ -10987,63 +11261,72 @@ "province_id": 138, "country_id": 103, "name": "Tohid", - "slug": "ir-ilam-tohid" + "slug": "ir-ilam-tohid", + "name_fa": "توحید" }, { "id": 1571, "province_id": 138, "country_id": 103, "name": "Zarneh", - "slug": "ir-ilam-zarneh" + "slug": "ir-ilam-zarneh", + "name_fa": "زرنه" }, { "id": 1572, "province_id": 139, "country_id": 103, "name": "Abouzeidabad", - "slug": "ir-isfahan-abouzeidabad" + "slug": "ir-isfahan-abouzeidabad", + "name_fa": "ابوزیدآباد" }, { "id": 1573, "province_id": 139, "country_id": 103, "name": "Abrisham", - "slug": "ir-isfahan-abrisham" + "slug": "ir-isfahan-abrisham", + "name_fa": "ابریشم" }, { "id": 1574, "province_id": 139, "country_id": 103, "name": "Afus", - "slug": "ir-isfahan-afus" + "slug": "ir-isfahan-afus", + "name_fa": "افوس" }, { "id": 1575, "province_id": 139, "country_id": 103, "name": "Alavijeh", - "slug": "ir-isfahan-alavijeh" + "slug": "ir-isfahan-alavijeh", + "name_fa": "علویجه" }, { "id": 1576, "province_id": 139, "country_id": 103, "name": "Anarak", - "slug": "ir-isfahan-anarak" + "slug": "ir-isfahan-anarak", + "name_fa": "انارک" }, { "id": 1577, "province_id": 139, "country_id": 103, "name": "Aran va Bidgol", - "slug": "ir-isfahan-aran-va-bidgol" + "slug": "ir-isfahan-aran-va-bidgol", + "name_fa": "آران وبیدگل" }, { "id": 1578, "province_id": 139, "country_id": 103, "name": "Ardestan", - "slug": "ir-isfahan-ardestan" + "slug": "ir-isfahan-ardestan", + "name_fa": "اردستان" }, { "id": 1579, @@ -11057,28 +11340,32 @@ "province_id": 139, "country_id": 103, "name": "Askaran", - "slug": "ir-isfahan-askaran" + "slug": "ir-isfahan-askaran", + "name_fa": "عسگران" }, { "id": 1581, "province_id": 139, "country_id": 103, "name": "Badroud", - "slug": "ir-isfahan-badroud" + "slug": "ir-isfahan-badroud", + "name_fa": "بادرود" }, { "id": 1582, "province_id": 139, "country_id": 103, "name": "Bafrān", - "slug": "ir-isfahan-bafr-n" + "slug": "ir-isfahan-bafr-n", + "name_fa": "بافران" }, { "id": 1583, "province_id": 139, "country_id": 103, "name": "Baghbahadoran", - "slug": "ir-isfahan-baghbahadoran" + "slug": "ir-isfahan-baghbahadoran", + "name_fa": "باغ بهادران" }, { "id": 1584, @@ -11092,28 +11379,32 @@ "province_id": 139, "country_id": 103, "name": "Baharan Shahr", - "slug": "ir-isfahan-baharan-shahr" + "slug": "ir-isfahan-baharan-shahr", + "name_fa": "بهاران شهر" }, { "id": 1586, "province_id": 139, "country_id": 103, "name": "Baharestan", - "slug": "ir-isfahan-baharestan" + "slug": "ir-isfahan-baharestan", + "name_fa": "بهارستان" }, { "id": 1587, "province_id": 139, "country_id": 103, "name": "Barf Anbar", - "slug": "ir-isfahan-barf-anbar" + "slug": "ir-isfahan-barf-anbar", + "name_fa": "برف انبار" }, { "id": 1588, "province_id": 139, "country_id": 103, "name": "Barzok", - "slug": "ir-isfahan-barzok" + "slug": "ir-isfahan-barzok", + "name_fa": "برزک" }, { "id": 1589, @@ -11127,63 +11418,72 @@ "province_id": 139, "country_id": 103, "name": "Buin va Miandasht", - "slug": "ir-isfahan-buin-va-miandasht" + "slug": "ir-isfahan-buin-va-miandasht", + "name_fa": "بوئین و میاندشت" }, { "id": 1591, "province_id": 139, "country_id": 103, "name": "Chadegan", - "slug": "ir-isfahan-chadegan" + "slug": "ir-isfahan-chadegan", + "name_fa": "چادگان" }, { "id": 1592, "province_id": 139, "country_id": 103, "name": "Chamgordan", - "slug": "ir-isfahan-chamgordan" + "slug": "ir-isfahan-chamgordan", + "name_fa": "چمگردان" }, { "id": 1593, "province_id": 139, "country_id": 103, "name": "Chermahin", - "slug": "ir-isfahan-chermahin" + "slug": "ir-isfahan-chermahin", + "name_fa": "چرمهین" }, { "id": 1594, "province_id": 139, "country_id": 103, "name": "Damaneh", - "slug": "ir-isfahan-damaneh" + "slug": "ir-isfahan-damaneh", + "name_fa": "دامنه" }, { "id": 1595, "province_id": 139, "country_id": 103, "name": "Daran", - "slug": "ir-isfahan-daran" + "slug": "ir-isfahan-daran", + "name_fa": "داران" }, { "id": 1596, "province_id": 139, "country_id": 103, "name": "Dastgerd", - "slug": "ir-isfahan-dastgerd" + "slug": "ir-isfahan-dastgerd", + "name_fa": "دستگرد" }, { "id": 1597, "province_id": 139, "country_id": 103, "name": "Dehagh", - "slug": "ir-isfahan-dehagh" + "slug": "ir-isfahan-dehagh", + "name_fa": "دهق" }, { "id": 1598, "province_id": 139, "country_id": 103, "name": "Dehaghan", - "slug": "ir-isfahan-dehaghan" + "slug": "ir-isfahan-dehaghan", + "name_fa": "دهاقان" }, { "id": 1599, @@ -11197,21 +11497,24 @@ "province_id": 139, "country_id": 103, "name": "Diziche", - "slug": "ir-isfahan-diziche" + "slug": "ir-isfahan-diziche", + "name_fa": "دیزیچه" }, { "id": 1601, "province_id": 139, "country_id": 103, "name": "Dolatabad", - "slug": "ir-isfahan-dolatabad" + "slug": "ir-isfahan-dolatabad", + "name_fa": "دولت آباد" }, { "id": 1602, "province_id": 139, "country_id": 103, "name": "Dorcheh", - "slug": "ir-isfahan-dorcheh" + "slug": "ir-isfahan-dorcheh", + "name_fa": "درچه" }, { "id": 1603, @@ -11232,21 +11535,24 @@ "province_id": 139, "country_id": 103, "name": "Esfahan", - "slug": "ir-isfahan-esfahan" + "slug": "ir-isfahan-esfahan", + "name_fa": "اصفهان" }, { "id": 1606, "province_id": 139, "country_id": 103, "name": "Ezhieh", - "slug": "ir-isfahan-ezhieh" + "slug": "ir-isfahan-ezhieh", + "name_fa": "اژیه" }, { "id": 1607, "province_id": 139, "country_id": 103, "name": "Falavarjan", - "slug": "ir-isfahan-falavarjan" + "slug": "ir-isfahan-falavarjan", + "name_fa": "فلاورجان" }, { "id": 1608, @@ -11260,7 +11566,8 @@ "province_id": 139, "country_id": 103, "name": "Farrokhi", - "slug": "ir-isfahan-farrokhi" + "slug": "ir-isfahan-farrokhi", + "name_fa": "فرخی" }, { "id": 1610, @@ -11281,49 +11588,56 @@ "province_id": 139, "country_id": 103, "name": "Fooladshahr", - "slug": "ir-isfahan-fooladshahr" + "slug": "ir-isfahan-fooladshahr", + "name_fa": "فولادشهر" }, { "id": 1613, "province_id": 139, "country_id": 103, "name": "Gazborkhar", - "slug": "ir-isfahan-gazborkhar" + "slug": "ir-isfahan-gazborkhar", + "name_fa": "گزبرخوار" }, { "id": 1614, "province_id": 139, "country_id": 103, "name": "Ghahderijan", - "slug": "ir-isfahan-ghahderijan" + "slug": "ir-isfahan-ghahderijan", + "name_fa": "قهدریجان" }, { "id": 1615, "province_id": 139, "country_id": 103, "name": "Ghamsar", - "slug": "ir-isfahan-ghamsar" + "slug": "ir-isfahan-ghamsar", + "name_fa": "قمصر" }, { "id": 1616, "province_id": 139, "country_id": 103, "name": "Goldasht", - "slug": "ir-isfahan-goldasht" + "slug": "ir-isfahan-goldasht", + "name_fa": "گلدشت" }, { "id": 1617, "province_id": 139, "country_id": 103, "name": "Golpayegan", - "slug": "ir-isfahan-golpayegan" + "slug": "ir-isfahan-golpayegan", + "name_fa": "گلپایگان" }, { "id": 1618, "province_id": 139, "country_id": 103, "name": "Golshahr", - "slug": "ir-isfahan-golshahr" + "slug": "ir-isfahan-golshahr", + "name_fa": "گلشهر" }, { "id": 1619, @@ -11337,21 +11651,24 @@ "province_id": 139, "country_id": 103, "name": "Googad", - "slug": "ir-isfahan-googad" + "slug": "ir-isfahan-googad", + "name_fa": "گوگد" }, { "id": 1621, "province_id": 139, "country_id": 103, "name": "Gorgab", - "slug": "ir-isfahan-gorgab" + "slug": "ir-isfahan-gorgab", + "name_fa": "گرگاب" }, { "id": 1622, "province_id": 139, "country_id": 103, "name": "Habibabad", - "slug": "ir-isfahan-habibabad" + "slug": "ir-isfahan-habibabad", + "name_fa": "حبیب آباد" }, { "id": 1623, @@ -11365,21 +11682,24 @@ "province_id": 139, "country_id": 103, "name": "Harand", - "slug": "ir-isfahan-harand" + "slug": "ir-isfahan-harand", + "name_fa": "هرند" }, { "id": 1625, "province_id": 139, "country_id": 103, "name": "Hassan Abad", - "slug": "ir-isfahan-hassan-abad" + "slug": "ir-isfahan-hassan-abad", + "name_fa": "حسن آباد" }, { "id": 1626, "province_id": 139, "country_id": 103, "name": "Imanshahr", - "slug": "ir-isfahan-imanshahr" + "slug": "ir-isfahan-imanshahr", + "name_fa": "ایمانشهر" }, { "id": 1627, @@ -11414,14 +11734,16 @@ "province_id": 139, "country_id": 103, "name": "Jowzdan", - "slug": "ir-isfahan-jowzdan" + "slug": "ir-isfahan-jowzdan", + "name_fa": "جوزدان" }, { "id": 1632, "province_id": 139, "country_id": 103, "name": "Kahrizsang", - "slug": "ir-isfahan-kahrizsang" + "slug": "ir-isfahan-kahrizsang", + "name_fa": "کهریزسنگ" }, { "id": 1633, @@ -11435,7 +11757,8 @@ "province_id": 139, "country_id": 103, "name": "Kashan", - "slug": "ir-isfahan-kashan" + "slug": "ir-isfahan-kashan", + "name_fa": "کاشان" }, { "id": 1635, @@ -11449,42 +11772,48 @@ "province_id": 139, "country_id": 103, "name": "Kelishad va Sudarjan", - "slug": "ir-isfahan-kelishad-va-sudarjan" + "slug": "ir-isfahan-kelishad-va-sudarjan", + "name_fa": "کلیشاد و سودرجان" }, { "id": 1637, "province_id": 139, "country_id": 103, "name": "Khaled Abad", - "slug": "ir-isfahan-khaled-abad" + "slug": "ir-isfahan-khaled-abad", + "name_fa": "خالدآباد" }, { "id": 1638, "province_id": 139, "country_id": 103, "name": "Khansar", - "slug": "ir-isfahan-khansar" + "slug": "ir-isfahan-khansar", + "name_fa": "خوانسار" }, { "id": 1639, "province_id": 139, "country_id": 103, "name": "Khomeyni Shahr", - "slug": "ir-isfahan-khomeyni-shahr" + "slug": "ir-isfahan-khomeyni-shahr", + "name_fa": "خمینی شهر" }, { "id": 1640, "province_id": 139, "country_id": 103, "name": "Khorzugh", - "slug": "ir-isfahan-khorzugh" + "slug": "ir-isfahan-khorzugh", + "name_fa": "خورزوق" }, { "id": 1641, "province_id": 139, "country_id": 103, "name": "Khur", - "slug": "ir-isfahan-khur" + "slug": "ir-isfahan-khur", + "name_fa": "خور" }, { "id": 1642, @@ -11505,28 +11834,32 @@ "province_id": 139, "country_id": 103, "name": "Komshecheh", - "slug": "ir-isfahan-komshecheh" + "slug": "ir-isfahan-komshecheh", + "name_fa": "کمشچه" }, { "id": 1645, "province_id": 139, "country_id": 103, "name": "Kooshk", - "slug": "ir-isfahan-kooshk" + "slug": "ir-isfahan-kooshk", + "name_fa": "کوشک" }, { "id": 1646, "province_id": 139, "country_id": 103, "name": "Kuhpayeh", - "slug": "ir-isfahan-kuhpayeh" + "slug": "ir-isfahan-kuhpayeh", + "name_fa": "کوهپایه" }, { "id": 1647, "province_id": 139, "country_id": 103, "name": "Laybid", - "slug": "ir-isfahan-laybid" + "slug": "ir-isfahan-laybid", + "name_fa": "لای بید" }, { "id": 1648, @@ -11540,14 +11873,16 @@ "province_id": 139, "country_id": 103, "name": "Mahabad", - "slug": "ir-isfahan-mahabad" + "slug": "ir-isfahan-mahabad", + "name_fa": "مهاباد" }, { "id": 1650, "province_id": 139, "country_id": 103, "name": "Majlesi", - "slug": "ir-isfahan-majlesi" + "slug": "ir-isfahan-majlesi", + "name_fa": "مجلسی" }, { "id": 1651, @@ -11561,7 +11896,8 @@ "province_id": 139, "country_id": 103, "name": "Mashkat", - "slug": "ir-isfahan-mashkat" + "slug": "ir-isfahan-mashkat", + "name_fa": "مشکات" }, { "id": 1653, @@ -11575,21 +11911,24 @@ "province_id": 139, "country_id": 103, "name": "Mobarakeh", - "slug": "ir-isfahan-mobarakeh" + "slug": "ir-isfahan-mobarakeh", + "name_fa": "مبارکه" }, { "id": 1655, "province_id": 139, "country_id": 103, "name": "Mohammad Abad", - "slug": "ir-isfahan-mohammad-abad" + "slug": "ir-isfahan-mohammad-abad", + "name_fa": "محمدآباد" }, { "id": 1656, "province_id": 139, "country_id": 103, "name": "Naein", - "slug": "ir-isfahan-naein" + "slug": "ir-isfahan-naein", + "name_fa": "نائین" }, { "id": 1657, @@ -11603,49 +11942,56 @@ "province_id": 139, "country_id": 103, "name": "Najafabad", - "slug": "ir-isfahan-najafabad" + "slug": "ir-isfahan-najafabad", + "name_fa": "نجف آباد" }, { "id": 1659, "province_id": 139, "country_id": 103, "name": "Nasr Abad", - "slug": "ir-isfahan-nasr-abad" + "slug": "ir-isfahan-nasr-abad", + "name_fa": "نصرآباد" }, { "id": 1660, "province_id": 139, "country_id": 103, "name": "Natanz", - "slug": "ir-isfahan-natanz" + "slug": "ir-isfahan-natanz", + "name_fa": "نطنز" }, { "id": 1661, "province_id": 139, "country_id": 103, "name": "Niasar", - "slug": "ir-isfahan-niasar" + "slug": "ir-isfahan-niasar", + "name_fa": "نیاسر" }, { "id": 1662, "province_id": 139, "country_id": 103, "name": "Nik Abad", - "slug": "ir-isfahan-nik-abad" + "slug": "ir-isfahan-nik-abad", + "name_fa": "نیک آباد" }, { "id": 1663, "province_id": 139, "country_id": 103, "name": "Nushabad", - "slug": "ir-isfahan-nushabad" + "slug": "ir-isfahan-nushabad", + "name_fa": "نوش آباد" }, { "id": 1664, "province_id": 139, "country_id": 103, "name": "Pir Bakran", - "slug": "ir-isfahan-pir-bakran" + "slug": "ir-isfahan-pir-bakran", + "name_fa": "پیربکران" }, { "id": 1665, @@ -11673,7 +12019,8 @@ "province_id": 139, "country_id": 103, "name": "Rezvanshahr", - "slug": "ir-isfahan-rezvanshahr" + "slug": "ir-isfahan-rezvanshahr", + "name_fa": "رضوانشهر" }, { "id": 1669, @@ -11687,14 +12034,16 @@ "province_id": 139, "country_id": 103, "name": "Sedeh Lenjan", - "slug": "ir-isfahan-sedeh-lenjan" + "slug": "ir-isfahan-sedeh-lenjan", + "name_fa": "سده لنجان" }, { "id": 1671, "province_id": 139, "country_id": 103, "name": "Sefid Shahr", - "slug": "ir-isfahan-sefid-shahr" + "slug": "ir-isfahan-sefid-shahr", + "name_fa": "سفیدشهر" }, { "id": 1672, @@ -11708,21 +12057,24 @@ "province_id": 139, "country_id": 103, "name": "Semirom", - "slug": "ir-isfahan-semirom" + "slug": "ir-isfahan-semirom", + "name_fa": "سمیرم" }, { "id": 1674, "province_id": 139, "country_id": 103, "name": "Shahin Shahr", - "slug": "ir-isfahan-shahin-shahr" + "slug": "ir-isfahan-shahin-shahr", + "name_fa": "شاهین شهر" }, { "id": 1675, "province_id": 139, "country_id": 103, "name": "Shahreza", - "slug": "ir-isfahan-shahreza" + "slug": "ir-isfahan-shahreza", + "name_fa": "شهرضا" }, { "id": 1676, @@ -11743,7 +12095,8 @@ "province_id": 139, "country_id": 103, "name": "Talkhooncheh", - "slug": "ir-isfahan-talkhooncheh" + "slug": "ir-isfahan-talkhooncheh", + "name_fa": "طالخونچه" }, { "id": 1679, @@ -11757,14 +12110,16 @@ "province_id": 139, "country_id": 103, "name": "Tiran", - "slug": "ir-isfahan-tiran" + "slug": "ir-isfahan-tiran", + "name_fa": "تیران" }, { "id": 1681, "province_id": 139, "country_id": 103, "name": "Toodeshk", - "slug": "ir-isfahan-toodeshk" + "slug": "ir-isfahan-toodeshk", + "name_fa": "تودشک" }, { "id": 1682, @@ -11778,7 +12133,8 @@ "province_id": 139, "country_id": 103, "name": "Varnamkhast", - "slug": "ir-isfahan-varnamkhast" + "slug": "ir-isfahan-varnamkhast", + "name_fa": "ورنامخواست" }, { "id": 1684, @@ -11799,7 +12155,8 @@ "province_id": 139, "country_id": 103, "name": "Zarinshahr", - "slug": "ir-isfahan-zarinshahr" + "slug": "ir-isfahan-zarinshahr", + "name_fa": "زرین شهر" }, { "id": 1687, @@ -11820,7 +12177,8 @@ "province_id": 139, "country_id": 103, "name": "Zayandehrod", - "slug": "ir-isfahan-zayandehrod" + "slug": "ir-isfahan-zayandehrod", + "name_fa": "زاینده رود" }, { "id": 1690, @@ -11841,56 +12199,64 @@ "province_id": 139, "country_id": 103, "name": "Zibashahr", - "slug": "ir-isfahan-zibashahr" + "slug": "ir-isfahan-zibashahr", + "name_fa": "زیباشهر" }, { "id": 1693, "province_id": 140, "country_id": 103, "name": "Aminshahr", - "slug": "ir-kerman-aminshahr" + "slug": "ir-kerman-aminshahr", + "name_fa": "امین شهر" }, { "id": 1694, "province_id": 140, "country_id": 103, "name": "Anar", - "slug": "ir-kerman-anar" + "slug": "ir-kerman-anar", + "name_fa": "انار" }, { "id": 1695, "province_id": 140, "country_id": 103, "name": "Anbarabad", - "slug": "ir-kerman-anbarabad" + "slug": "ir-kerman-anbarabad", + "name_fa": "عنبرآباد" }, { "id": 1696, "province_id": 140, "country_id": 103, "name": "Andouhjerd", - "slug": "ir-kerman-andouhjerd" + "slug": "ir-kerman-andouhjerd", + "name_fa": "اندوهجرد" }, { "id": 1697, "province_id": 140, "country_id": 103, "name": "Baft", - "slug": "ir-kerman-baft" + "slug": "ir-kerman-baft", + "name_fa": "بافت" }, { "id": 1698, "province_id": 140, "country_id": 103, "name": "Bagheyn", - "slug": "ir-kerman-bagheyn" + "slug": "ir-kerman-bagheyn", + "name_fa": "باغین" }, { "id": 1699, "province_id": 140, "country_id": 103, "name": "Bahreman", - "slug": "ir-kerman-bahreman" + "slug": "ir-kerman-bahreman", + "name_fa": "بهرمان" }, { "id": 1700, @@ -11904,7 +12270,8 @@ "province_id": 140, "country_id": 103, "name": "Bam", - "slug": "ir-kerman-bam" + "slug": "ir-kerman-bam", + "name_fa": "بم" }, { "id": 1702, @@ -11918,14 +12285,16 @@ "province_id": 140, "country_id": 103, "name": "Bardsir", - "slug": "ir-kerman-bardsir" + "slug": "ir-kerman-bardsir", + "name_fa": "بردسیر" }, { "id": 1704, "province_id": 140, "country_id": 103, "name": "Bezanjan", - "slug": "ir-kerman-bezanjan" + "slug": "ir-kerman-bezanjan", + "name_fa": "بزنجان" }, { "id": 1705, @@ -11939,14 +12308,16 @@ "province_id": 140, "country_id": 103, "name": "Chatroud", - "slug": "ir-kerman-chatroud" + "slug": "ir-kerman-chatroud", + "name_fa": "چترود" }, { "id": 1707, "province_id": 140, "country_id": 103, "name": "Darb-e Behesht", - "slug": "ir-kerman-darb-e-behesht" + "slug": "ir-kerman-darb-e-behesht", + "name_fa": "درب بهشت" }, { "id": 1708, @@ -11974,42 +12345,48 @@ "province_id": 140, "country_id": 103, "name": "Ekhteyarabad", - "slug": "ir-kerman-ekhteyarabad" + "slug": "ir-kerman-ekhteyarabad", + "name_fa": "اختیارآباد" }, { "id": 1712, "province_id": 140, "country_id": 103, "name": "Fahraj", - "slug": "ir-kerman-fahraj" + "slug": "ir-kerman-fahraj", + "name_fa": "فهرج" }, { "id": 1713, "province_id": 140, "country_id": 103, "name": "Faryab", - "slug": "ir-kerman-faryab" + "slug": "ir-kerman-faryab", + "name_fa": "فاریاب" }, { "id": 1714, "province_id": 140, "country_id": 103, "name": "Ghalehganj", - "slug": "ir-kerman-ghalehganj" + "slug": "ir-kerman-ghalehganj", + "name_fa": "قلعه گنج" }, { "id": 1715, "province_id": 140, "country_id": 103, "name": "Golbāf", - "slug": "ir-kerman-golba-f" + "slug": "ir-kerman-golba-f", + "name_fa": "گلباف" }, { "id": 1716, "province_id": 140, "country_id": 103, "name": "Golzar", - "slug": "ir-kerman-golzar" + "slug": "ir-kerman-golzar", + "name_fa": "گلزار" }, { "id": 1717, @@ -12030,7 +12407,8 @@ "province_id": 140, "country_id": 103, "name": "Hojedk", - "slug": "ir-kerman-hojedk" + "slug": "ir-kerman-hojedk", + "name_fa": "هجدک" }, { "id": 1720, @@ -12044,35 +12422,40 @@ "province_id": 140, "country_id": 103, "name": "Jebalbarez", - "slug": "ir-kerman-jebalbarez" + "slug": "ir-kerman-jebalbarez", + "name_fa": "جبالبارز" }, { "id": 1722, "province_id": 140, "country_id": 103, "name": "Jiroft", - "slug": "ir-kerman-jiroft" + "slug": "ir-kerman-jiroft", + "name_fa": "جیرفت" }, { "id": 1723, "province_id": 140, "country_id": 103, "name": "Joupar", - "slug": "ir-kerman-joupar" + "slug": "ir-kerman-joupar", + "name_fa": "جوپار" }, { "id": 1724, "province_id": 140, "country_id": 103, "name": "Jowzam", - "slug": "ir-kerman-jowzam" + "slug": "ir-kerman-jowzam", + "name_fa": "جوزم" }, { "id": 1725, "province_id": 140, "country_id": 103, "name": "Kahnooj", - "slug": "ir-kerman-kahnooj" + "slug": "ir-kerman-kahnooj", + "name_fa": "کهنوج" }, { "id": 1726, @@ -12086,14 +12469,16 @@ "province_id": 140, "country_id": 103, "name": "Kazem Abad", - "slug": "ir-kerman-kazem-abad" + "slug": "ir-kerman-kazem-abad", + "name_fa": "کاظم آباد" }, { "id": 1728, "province_id": 140, "country_id": 103, "name": "Kerman", - "slug": "ir-kerman-kerman" + "slug": "ir-kerman-kerman", + "name_fa": "کرمان" }, { "id": 1729, @@ -12107,28 +12492,32 @@ "province_id": 140, "country_id": 103, "name": "Khanook", - "slug": "ir-kerman-khanook" + "slug": "ir-kerman-khanook", + "name_fa": "خانوک" }, { "id": 1731, "province_id": 140, "country_id": 103, "name": "KhatonAbad", - "slug": "ir-kerman-khatonabad" + "slug": "ir-kerman-khatonabad", + "name_fa": "خاتون آباد" }, { "id": 1732, "province_id": 140, "country_id": 103, "name": "Khursand", - "slug": "ir-kerman-khursand" + "slug": "ir-kerman-khursand", + "name_fa": "خورسند" }, { "id": 1733, "province_id": 140, "country_id": 103, "name": "Kian Shahr", - "slug": "ir-kerman-kian-shahr" + "slug": "ir-kerman-kian-shahr", + "name_fa": "کیانشهر" }, { "id": 1734, @@ -12149,28 +12538,32 @@ "province_id": 140, "country_id": 103, "name": "Kuhbanan", - "slug": "ir-kerman-kuhbanan" + "slug": "ir-kerman-kuhbanan", + "name_fa": "کوهبنان" }, { "id": 1737, "province_id": 140, "country_id": 103, "name": "Laleh zaar", - "slug": "ir-kerman-laleh-zaar" + "slug": "ir-kerman-laleh-zaar", + "name_fa": "لاله زار" }, { "id": 1738, "province_id": 140, "country_id": 103, "name": "Mahan", - "slug": "ir-kerman-mahan" + "slug": "ir-kerman-mahan", + "name_fa": "ماهان" }, { "id": 1739, "province_id": 140, "country_id": 103, "name": "Manoojan", - "slug": "ir-kerman-manoojan" + "slug": "ir-kerman-manoojan", + "name_fa": "منوجان" }, { "id": 1740, @@ -12184,49 +12577,56 @@ "province_id": 140, "country_id": 103, "name": "Mardehak", - "slug": "ir-kerman-mardehak" + "slug": "ir-kerman-mardehak", + "name_fa": "مردهک" }, { "id": 1742, "province_id": 140, "country_id": 103, "name": "Mohammadabad", - "slug": "ir-kerman-mohammadabad" + "slug": "ir-kerman-mohammadabad", + "name_fa": "محمدآباد" }, { "id": 1743, "province_id": 140, "country_id": 103, "name": "MohyAbad", - "slug": "ir-kerman-mohyabad" + "slug": "ir-kerman-mohyabad", + "name_fa": "محی آباد" }, { "id": 1744, "province_id": 140, "country_id": 103, "name": "Najaf Shahr", - "slug": "ir-kerman-najaf-shahr" + "slug": "ir-kerman-najaf-shahr", + "name_fa": "نجف شهر" }, { "id": 1745, "province_id": 140, "country_id": 103, "name": "Narmashir", - "slug": "ir-kerman-narmashir" + "slug": "ir-kerman-narmashir", + "name_fa": "نرماشیر" }, { "id": 1746, "province_id": 140, "country_id": 103, "name": "Negar", - "slug": "ir-kerman-negar" + "slug": "ir-kerman-negar", + "name_fa": "نگار" }, { "id": 1747, "province_id": 140, "country_id": 103, "name": "Nezamshahr", - "slug": "ir-kerman-nezamshahr" + "slug": "ir-kerman-nezamshahr", + "name_fa": "نظام شهر" }, { "id": 1748, @@ -12254,7 +12654,8 @@ "province_id": 140, "country_id": 103, "name": "Pariz", - "slug": "ir-kerman-pariz" + "slug": "ir-kerman-pariz", + "name_fa": "پاریز" }, { "id": 1752, @@ -12268,14 +12669,16 @@ "province_id": 140, "country_id": 103, "name": "Rabor", - "slug": "ir-kerman-rabor" + "slug": "ir-kerman-rabor", + "name_fa": "رابر" }, { "id": 1754, "province_id": 140, "country_id": 103, "name": "Rafsanjan", - "slug": "ir-kerman-rafsanjan" + "slug": "ir-kerman-rafsanjan", + "name_fa": "رفسنجان" }, { "id": 1755, @@ -12310,7 +12713,8 @@ "province_id": 140, "country_id": 103, "name": "Rudbar", - "slug": "ir-kerman-rudbar" + "slug": "ir-kerman-rudbar", + "name_fa": "رودبار" }, { "id": 1760, @@ -12324,7 +12728,8 @@ "province_id": 140, "country_id": 103, "name": "Shahdad", - "slug": "ir-kerman-shahdad" + "slug": "ir-kerman-shahdad", + "name_fa": "شهداد" }, { "id": 1762, @@ -12338,35 +12743,40 @@ "province_id": 140, "country_id": 103, "name": "Shahrbabak", - "slug": "ir-kerman-shahrbabak" + "slug": "ir-kerman-shahrbabak", + "name_fa": "شهربابک" }, { "id": 1764, "province_id": 140, "country_id": 103, "name": "Sirjan", - "slug": "ir-kerman-sirjan" + "slug": "ir-kerman-sirjan", + "name_fa": "سیرجان" }, { "id": 1765, "province_id": 140, "country_id": 103, "name": "Yazdan Shahr", - "slug": "ir-kerman-yazdan-shahr" + "slug": "ir-kerman-yazdan-shahr", + "name_fa": "یزدان شهر" }, { "id": 1766, "province_id": 140, "country_id": 103, "name": "Zangiabad", - "slug": "ir-kerman-zangiabad" + "slug": "ir-kerman-zangiabad", + "name_fa": "زنگی آباد" }, { "id": 1767, "province_id": 140, "country_id": 103, "name": "Zarand", - "slug": "ir-kerman-zarand" + "slug": "ir-kerman-zarand", + "name_fa": "زرند" }, { "id": 1768, @@ -12380,14 +12790,16 @@ "province_id": 140, "country_id": 103, "name": "Zeydabad", - "slug": "ir-kerman-zeydabad" + "slug": "ir-kerman-zeydabad", + "name_fa": "زیدآباد" }, { "id": 1770, "province_id": 141, "country_id": 103, "name": "azeh Abad", - "slug": "ir-kermanshah-azeh-abad" + "slug": "ir-kermanshah-azeh-abad", + "name_fa": "تازه آباد" }, { "id": 1771, @@ -12401,14 +12813,16 @@ "province_id": 141, "country_id": 103, "name": "Bayangan", - "slug": "ir-kermanshah-bayangan" + "slug": "ir-kermanshah-bayangan", + "name_fa": "باینگان" }, { "id": 1773, "province_id": 141, "country_id": 103, "name": "Bisotun", - "slug": "ir-kermanshah-bisotun" + "slug": "ir-kermanshah-bisotun", + "name_fa": "بیستون" }, { "id": 1774, @@ -12422,7 +12836,8 @@ "province_id": 141, "country_id": 103, "name": "Eslamabad-e-Gharb", - "slug": "ir-kermanshah-eslamabad-e-gharb" + "slug": "ir-kermanshah-eslamabad-e-gharb", + "name_fa": "اسلام آباد غرب" }, { "id": 1776, @@ -12436,14 +12851,16 @@ "province_id": 141, "country_id": 103, "name": "Gahvareh", - "slug": "ir-kermanshah-gahvareh" + "slug": "ir-kermanshah-gahvareh", + "name_fa": "گهواره" }, { "id": 1778, "province_id": 141, "country_id": 103, "name": "Gilan Gharb", - "slug": "ir-kermanshah-gilan-gharb" + "slug": "ir-kermanshah-gilan-gharb", + "name_fa": "گیلانغرب" }, { "id": 1779, @@ -12457,28 +12874,32 @@ "province_id": 141, "country_id": 103, "name": "Halashi", - "slug": "ir-kermanshah-halashi" + "slug": "ir-kermanshah-halashi", + "name_fa": "هلشی" }, { "id": 1781, "province_id": 141, "country_id": 103, "name": "Hamil", - "slug": "ir-kermanshah-hamil" + "slug": "ir-kermanshah-hamil", + "name_fa": "حمیل" }, { "id": 1782, "province_id": 141, "country_id": 103, "name": "Harsin", - "slug": "ir-kermanshah-harsin" + "slug": "ir-kermanshah-harsin", + "name_fa": "هرسین" }, { "id": 1783, "province_id": 141, "country_id": 103, "name": "Javanrud", - "slug": "ir-kermanshah-javanrud" + "slug": "ir-kermanshah-javanrud", + "name_fa": "جوان رود" }, { "id": 1784, @@ -12492,28 +12913,32 @@ "province_id": 141, "country_id": 103, "name": "Kangavar", - "slug": "ir-kermanshah-kangavar" + "slug": "ir-kermanshah-kangavar", + "name_fa": "کنگاور" }, { "id": 1786, "province_id": 141, "country_id": 103, "name": "Kerend-e Gharb", - "slug": "ir-kermanshah-kerend-e-gharb" + "slug": "ir-kermanshah-kerend-e-gharb", + "name_fa": "کرندغرب" }, { "id": 1787, "province_id": 141, "country_id": 103, "name": "Kermanshah", - "slug": "ir-kermanshah-kermanshah" + "slug": "ir-kermanshah-kermanshah", + "name_fa": "کرمانشاه" }, { "id": 1788, "province_id": 141, "country_id": 103, "name": "Kuzaran", - "slug": "ir-kermanshah-kuzaran" + "slug": "ir-kermanshah-kuzaran", + "name_fa": "کوزران" }, { "id": 1789, @@ -12527,21 +12952,24 @@ "province_id": 141, "country_id": 103, "name": "Miyan Rahan", - "slug": "ir-kermanshah-miyan-rahan" + "slug": "ir-kermanshah-miyan-rahan", + "name_fa": "میان راهان" }, { "id": 1791, "province_id": 141, "country_id": 103, "name": "Nowdeshah", - "slug": "ir-kermanshah-nowdeshah" + "slug": "ir-kermanshah-nowdeshah", + "name_fa": "نودشه" }, { "id": 1792, "province_id": 141, "country_id": 103, "name": "Nowsud", - "slug": "ir-kermanshah-nowsud" + "slug": "ir-kermanshah-nowsud", + "name_fa": "نوسود" }, { "id": 1793, @@ -12555,14 +12983,16 @@ "province_id": 141, "country_id": 103, "name": "Qasr-e Shirin", - "slug": "ir-kermanshah-qasr-e-shirin" + "slug": "ir-kermanshah-qasr-e-shirin", + "name_fa": "قصرشیرین" }, { "id": 1795, "province_id": 141, "country_id": 103, "name": "Ravansar", - "slug": "ir-kermanshah-ravansar" + "slug": "ir-kermanshah-ravansar", + "name_fa": "روانسر" }, { "id": 1796, @@ -12583,7 +13013,8 @@ "province_id": 141, "country_id": 103, "name": "Sahneh", - "slug": "ir-kermanshah-sahneh" + "slug": "ir-kermanshah-sahneh", + "name_fa": "صحنه" }, { "id": 1799, @@ -12597,7 +13028,8 @@ "province_id": 141, "country_id": 103, "name": "Sarpol Zahab", - "slug": "ir-kermanshah-sarpol-zahab" + "slug": "ir-kermanshah-sarpol-zahab", + "name_fa": "سرپل ذهاب" }, { "id": 1801, @@ -12618,21 +13050,24 @@ "province_id": 141, "country_id": 103, "name": "Soomar", - "slug": "ir-kermanshah-soomar" + "slug": "ir-kermanshah-soomar", + "name_fa": "سومار" }, { "id": 1804, "province_id": 141, "country_id": 103, "name": "Srmast", - "slug": "ir-kermanshah-srmast" + "slug": "ir-kermanshah-srmast", + "name_fa": "سرمست" }, { "id": 1805, "province_id": 142, "country_id": 103, "name": "Abadan", - "slug": "ir-khuzestan-abadan" + "slug": "ir-khuzestan-abadan", + "name_fa": "آبادان" }, { "id": 1806, @@ -12653,21 +13088,24 @@ "province_id": 142, "country_id": 103, "name": "Aghajari", - "slug": "ir-khuzestan-aghajari" + "slug": "ir-khuzestan-aghajari", + "name_fa": "آغاجاری" }, { "id": 1809, "province_id": 142, "country_id": 103, "name": "Ahvaz", - "slug": "ir-khuzestan-ahvaz" + "slug": "ir-khuzestan-ahvaz", + "name_fa": "اهواز" }, { "id": 1810, "province_id": 142, "country_id": 103, "name": "Alvan", - "slug": "ir-khuzestan-alvan" + "slug": "ir-khuzestan-alvan", + "name_fa": "الوان" }, { "id": 1811, @@ -12681,14 +13119,16 @@ "province_id": 142, "country_id": 103, "name": "Andimeshk", - "slug": "ir-khuzestan-andimeshk" + "slug": "ir-khuzestan-andimeshk", + "name_fa": "اندیمشک" }, { "id": 1813, "province_id": 142, "country_id": 103, "name": "Arvand Kenar", - "slug": "ir-khuzestan-arvand-kenar" + "slug": "ir-khuzestan-arvand-kenar", + "name_fa": "اروندکنار" }, { "id": 1814, @@ -12702,21 +13142,24 @@ "province_id": 142, "country_id": 103, "name": "Bagh Malek", - "slug": "ir-khuzestan-bagh-malek" + "slug": "ir-khuzestan-bagh-malek", + "name_fa": "باغ ملک" }, { "id": 1816, "province_id": 142, "country_id": 103, "name": "Bandar Imam Khomeini", - "slug": "ir-khuzestan-bandar-imam-khomeini" + "slug": "ir-khuzestan-bandar-imam-khomeini", + "name_fa": "بندرامام خمینی" }, { "id": 1817, "province_id": 142, "country_id": 103, "name": "Bandar Mahshahr", - "slug": "ir-khuzestan-bandar-mahshahr" + "slug": "ir-khuzestan-bandar-mahshahr", + "name_fa": "بندرماهشهر" }, { "id": 1818, @@ -12730,7 +13173,8 @@ "province_id": 142, "country_id": 103, "name": "Behbahan", - "slug": "ir-khuzestan-behbahan" + "slug": "ir-khuzestan-behbahan", + "name_fa": "بهبهان" }, { "id": 1820, @@ -12744,7 +13188,8 @@ "province_id": 142, "country_id": 103, "name": "Bostan", - "slug": "ir-khuzestan-bostan" + "slug": "ir-khuzestan-bostan", + "name_fa": "بستان" }, { "id": 1822, @@ -12765,7 +13210,8 @@ "province_id": 142, "country_id": 103, "name": "Chavibdeh", - "slug": "ir-khuzestan-chavibdeh" + "slug": "ir-khuzestan-chavibdeh", + "name_fa": "چویبده" }, { "id": 1825, @@ -12779,21 +13225,24 @@ "province_id": 142, "country_id": 103, "name": "Darkhovin", - "slug": "ir-khuzestan-darkhovin" + "slug": "ir-khuzestan-darkhovin", + "name_fa": "دارخوین" }, { "id": 1827, "province_id": 142, "country_id": 103, "name": "Dehdez", - "slug": "ir-khuzestan-dehdez" + "slug": "ir-khuzestan-dehdez", + "name_fa": "دهدز" }, { "id": 1828, "province_id": 142, "country_id": 103, "name": "Dezful", - "slug": "ir-khuzestan-dezful" + "slug": "ir-khuzestan-dezful", + "name_fa": "دزفول" }, { "id": 1829, @@ -12849,7 +13298,8 @@ "province_id": 142, "country_id": 103, "name": "Hamidiyeh", - "slug": "ir-khuzestan-hamidiyeh" + "slug": "ir-khuzestan-hamidiyeh", + "name_fa": "امیدیه" }, { "id": 1837, @@ -12863,7 +13313,8 @@ "province_id": 142, "country_id": 103, "name": "Hendijan", - "slug": "ir-khuzestan-hendijan" + "slug": "ir-khuzestan-hendijan", + "name_fa": "هندیجان" }, { "id": 1839, @@ -12919,7 +13370,8 @@ "province_id": 142, "country_id": 103, "name": "Jayezan", - "slug": "ir-khuzestan-jayezan" + "slug": "ir-khuzestan-jayezan", + "name_fa": "جایزان" }, { "id": 1847, @@ -12940,7 +13392,8 @@ "province_id": 142, "country_id": 103, "name": "Khorramshahr", - "slug": "ir-khuzestan-khorramshahr" + "slug": "ir-khuzestan-khorramshahr", + "name_fa": "خرمشهر" }, { "id": 1850, @@ -12968,7 +13421,8 @@ "province_id": 142, "country_id": 103, "name": "Lali", - "slug": "ir-khuzestan-lali" + "slug": "ir-khuzestan-lali", + "name_fa": "لالی" }, { "id": 1854, @@ -12982,14 +13436,16 @@ "province_id": 142, "country_id": 103, "name": "Masjed Soleyman", - "slug": "ir-khuzestan-masjed-soleyman" + "slug": "ir-khuzestan-masjed-soleyman", + "name_fa": "مسجدسلیمان" }, { "id": 1856, "province_id": 142, "country_id": 103, "name": "Mianrood", - "slug": "ir-khuzestan-mianrood" + "slug": "ir-khuzestan-mianrood", + "name_fa": "میانرود" }, { "id": 1857, @@ -13003,14 +13459,16 @@ "province_id": 142, "country_id": 103, "name": "Minushahr", - "slug": "ir-khuzestan-minushahr" + "slug": "ir-khuzestan-minushahr", + "name_fa": "مینوشهر" }, { "id": 1859, "province_id": 142, "country_id": 103, "name": "Mollasani", - "slug": "ir-khuzestan-mollasani" + "slug": "ir-khuzestan-mollasani", + "name_fa": "ملاثانی" }, { "id": 1860, @@ -13031,7 +13489,8 @@ "province_id": 142, "country_id": 103, "name": "Omidiyeh", - "slug": "ir-khuzestan-omidiyeh" + "slug": "ir-khuzestan-omidiyeh", + "name_fa": "حمیدیه" }, { "id": 1863, @@ -13045,14 +13504,16 @@ "province_id": 142, "country_id": 103, "name": "Ramhormoz", - "slug": "ir-khuzestan-ramhormoz" + "slug": "ir-khuzestan-ramhormoz", + "name_fa": "رامهرمز" }, { "id": 1865, "province_id": 142, "country_id": 103, "name": "Ramshir", - "slug": "ir-khuzestan-ramshir" + "slug": "ir-khuzestan-ramshir", + "name_fa": "رامشیر" }, { "id": 1866, @@ -13066,28 +13527,32 @@ "province_id": 142, "country_id": 103, "name": "Safiabad", - "slug": "ir-khuzestan-safiabad" + "slug": "ir-khuzestan-safiabad", + "name_fa": "صفی آباد" }, { "id": 1868, "province_id": 142, "country_id": 103, "name": "Sāland", - "slug": "ir-khuzestan-s-land" + "slug": "ir-khuzestan-s-land", + "name_fa": "سالند" }, { "id": 1869, "province_id": 142, "country_id": 103, "name": "Salehshahr", - "slug": "ir-khuzestan-salehshahr" + "slug": "ir-khuzestan-salehshahr", + "name_fa": "صالح شهر" }, { "id": 1870, "province_id": 142, "country_id": 103, "name": "Sar Dasht", - "slug": "ir-khuzestan-sar-dasht" + "slug": "ir-khuzestan-sar-dasht", + "name_fa": "سردشت" }, { "id": 1871, @@ -13115,7 +13580,8 @@ "province_id": 142, "country_id": 103, "name": "Shadegan", - "slug": "ir-khuzestan-shadegan" + "slug": "ir-khuzestan-shadegan", + "name_fa": "شادگان" }, { "id": 1875, @@ -13129,28 +13595,32 @@ "province_id": 142, "country_id": 103, "name": "Sherafat", - "slug": "ir-khuzestan-sherafat" + "slug": "ir-khuzestan-sherafat", + "name_fa": "شرافت" }, { "id": 1877, "province_id": 142, "country_id": 103, "name": "Sheyban", - "slug": "ir-khuzestan-sheyban" + "slug": "ir-khuzestan-sheyban", + "name_fa": "شیبان" }, { "id": 1878, "province_id": 142, "country_id": 103, "name": "Shooshtar", - "slug": "ir-khuzestan-shooshtar" + "slug": "ir-khuzestan-shooshtar", + "name_fa": "شوشتر" }, { "id": 1879, "province_id": 142, "country_id": 103, "name": "Shush", - "slug": "ir-khuzestan-shush" + "slug": "ir-khuzestan-shush", + "name_fa": "شوش" }, { "id": 1880, @@ -13171,7 +13641,8 @@ "province_id": 142, "country_id": 103, "name": "Susangerd", - "slug": "ir-khuzestan-susangerd" + "slug": "ir-khuzestan-susangerd", + "name_fa": "سوسنگرد" }, { "id": 1883, @@ -13185,7 +13656,8 @@ "province_id": 142, "country_id": 103, "name": "Torkalaki", - "slug": "ir-khuzestan-torkalaki" + "slug": "ir-khuzestan-torkalaki", + "name_fa": "ترکالکی" }, { "id": 1885, @@ -13199,7 +13671,8 @@ "province_id": 142, "country_id": 103, "name": "Zahreh", - "slug": "ir-khuzestan-zahreh" + "slug": "ir-khuzestan-zahreh", + "name_fa": "زهره" }, { "id": 1887, @@ -13213,21 +13686,24 @@ "province_id": 143, "country_id": 103, "name": "Basht", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-basht" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-basht", + "name_fa": "باشت" }, { "id": 1889, "province_id": 143, "country_id": 103, "name": "Charam", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-charam" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-charam", + "name_fa": "چرام" }, { "id": 1890, "province_id": 143, "country_id": 103, "name": "Chitab", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-chitab" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-chitab", + "name_fa": "چیتاب" }, { "id": 1891, @@ -13241,21 +13717,24 @@ "province_id": 143, "country_id": 103, "name": "Dehdasht", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-dehdasht" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-dehdasht", + "name_fa": "دهدشت" }, { "id": 1893, "province_id": 143, "country_id": 103, "name": "Dishmok", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-dishmok" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-dishmok", + "name_fa": "دیشموک" }, { "id": 1894, "province_id": 143, "country_id": 103, "name": "Dogonbadan", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-dogonbadan" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-dogonbadan", + "name_fa": "دوگنبدان" }, { "id": 1895, @@ -13269,7 +13748,8 @@ "province_id": 143, "country_id": 103, "name": "Garab-e Sofla", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-garab-e-sofla" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-garab-e-sofla", + "name_fa": "گراب سفلی" }, { "id": 1897, @@ -13283,7 +13763,8 @@ "province_id": 143, "country_id": 103, "name": "Landeh", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-landeh" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-landeh", + "name_fa": "لنده" }, { "id": 1899, @@ -13297,28 +13778,32 @@ "province_id": 143, "country_id": 103, "name": "Likak", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-likak" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-likak", + "name_fa": "لیکک" }, { "id": 1901, "province_id": 143, "country_id": 103, "name": "Madavan", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-madavan" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-madavan", + "name_fa": "مادوان" }, { "id": 1902, "province_id": 143, "country_id": 103, "name": "Margoon", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-margoon" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-margoon", + "name_fa": "مارگون" }, { "id": 1903, "province_id": 143, "country_id": 103, "name": "Pataveh", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-pataveh" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-pataveh", + "name_fa": "پاتاوه" }, { "id": 1904, @@ -13339,7 +13824,8 @@ "province_id": 143, "country_id": 103, "name": "Sisakht", - "slug": "ir-kohgiluyeh-and-boyer-ahmad-sisakht" + "slug": "ir-kohgiluyeh-and-boyer-ahmad-sisakht", + "name_fa": "سی سخت" }, { "id": 1907, @@ -13360,21 +13846,24 @@ "province_id": 144, "country_id": 103, "name": "Armardeh", - "slug": "ir-kurdistan-armardeh" + "slug": "ir-kurdistan-armardeh", + "name_fa": "آرمرده" }, { "id": 1910, "province_id": 144, "country_id": 103, "name": "Babarashani", - "slug": "ir-kurdistan-babarashani" + "slug": "ir-kurdistan-babarashani", + "name_fa": "بابارشانی" }, { "id": 1911, "province_id": 144, "country_id": 103, "name": "Baneh", - "slug": "ir-kurdistan-baneh" + "slug": "ir-kurdistan-baneh", + "name_fa": "بانه" }, { "id": 1912, @@ -13388,7 +13877,8 @@ "province_id": 144, "country_id": 103, "name": "Bijar", - "slug": "ir-kurdistan-bijar" + "slug": "ir-kurdistan-bijar", + "name_fa": "بیجار" }, { "id": 1914, @@ -13402,28 +13892,32 @@ "province_id": 144, "country_id": 103, "name": "Bolbanabad", - "slug": "ir-kurdistan-bolbanabad" + "slug": "ir-kurdistan-bolbanabad", + "name_fa": "بلبان آباد" }, { "id": 1916, "province_id": 144, "country_id": 103, "name": "Chnare", - "slug": "ir-kurdistan-chnare" + "slug": "ir-kurdistan-chnare", + "name_fa": "چناره" }, { "id": 1917, "province_id": 144, "country_id": 103, "name": "Dehgolan", - "slug": "ir-kurdistan-dehgolan" + "slug": "ir-kurdistan-dehgolan", + "name_fa": "دهگلان" }, { "id": 1918, "province_id": 144, "country_id": 103, "name": "Delbaran", - "slug": "ir-kurdistan-delbaran" + "slug": "ir-kurdistan-delbaran", + "name_fa": "دلبران" }, { "id": 1919, @@ -13437,7 +13931,8 @@ "province_id": 144, "country_id": 103, "name": "Divandarreh", - "slug": "ir-kurdistan-divandarreh" + "slug": "ir-kurdistan-divandarreh", + "name_fa": "دیواندره" }, { "id": 1921, @@ -13451,35 +13946,40 @@ "province_id": 144, "country_id": 103, "name": "Kamyaran", - "slug": "ir-kurdistan-kamyaran" + "slug": "ir-kurdistan-kamyaran", + "name_fa": "کامیاران" }, { "id": 1923, "province_id": 144, "country_id": 103, "name": "Kani Dinar", - "slug": "ir-kurdistan-kani-dinar" + "slug": "ir-kurdistan-kani-dinar", + "name_fa": "کانی دینار" }, { "id": 1924, "province_id": 144, "country_id": 103, "name": "Kani Sur", - "slug": "ir-kurdistan-kani-sur" + "slug": "ir-kurdistan-kani-sur", + "name_fa": "کانی سور" }, { "id": 1925, "province_id": 144, "country_id": 103, "name": "Marivan", - "slug": "ir-kurdistan-marivan" + "slug": "ir-kurdistan-marivan", + "name_fa": "مریوان" }, { "id": 1926, "province_id": 144, "country_id": 103, "name": "Muchesh", - "slug": "ir-kurdistan-muchesh" + "slug": "ir-kurdistan-muchesh", + "name_fa": "موچش" }, { "id": 1927, @@ -13500,14 +14000,16 @@ "province_id": 144, "country_id": 103, "name": "Saheb", - "slug": "ir-kurdistan-saheb" + "slug": "ir-kurdistan-saheb", + "name_fa": "صاحب" }, { "id": 1930, "province_id": 144, "country_id": 103, "name": "Sanandaj", - "slug": "ir-kurdistan-sanandaj" + "slug": "ir-kurdistan-sanandaj", + "name_fa": "سنندج" }, { "id": 1931, @@ -13521,14 +14023,16 @@ "province_id": 144, "country_id": 103, "name": "Sarvabad", - "slug": "ir-kurdistan-sarvabad" + "slug": "ir-kurdistan-sarvabad", + "name_fa": "سروآباد" }, { "id": 1933, "province_id": 144, "country_id": 103, "name": "Serish Abad", - "slug": "ir-kurdistan-serish-abad" + "slug": "ir-kurdistan-serish-abad", + "name_fa": "سریش آباد" }, { "id": 1934, @@ -13556,7 +14060,8 @@ "province_id": 144, "country_id": 103, "name": "Zarrineh", - "slug": "ir-kurdistan-zarrineh" + "slug": "ir-kurdistan-zarrineh", + "name_fa": "زرینه" }, { "id": 1938, @@ -13619,7 +14124,8 @@ "province_id": 145, "country_id": 103, "name": "Delfan", - "slug": "ir-lorestan-delfan" + "slug": "ir-lorestan-delfan", + "name_fa": "دیلمان" }, { "id": 1947, @@ -13780,21 +14286,24 @@ "province_id": 146, "country_id": 103, "name": "Arak", - "slug": "ir-markazi-arak" + "slug": "ir-markazi-arak", + "name_fa": "اراک" }, { "id": 1970, "province_id": 146, "country_id": 103, "name": "Ashtian", - "slug": "ir-markazi-ashtian" + "slug": "ir-markazi-ashtian", + "name_fa": "آشتیان" }, { "id": 1971, "province_id": 146, "country_id": 103, "name": "Astaneh", - "slug": "ir-markazi-astaneh" + "slug": "ir-markazi-astaneh", + "name_fa": "آستانه" }, { "id": 1972, @@ -13808,14 +14317,16 @@ "province_id": 146, "country_id": 103, "name": "Davoud Abad", - "slug": "ir-markazi-davoud-abad" + "slug": "ir-markazi-davoud-abad", + "name_fa": "داودآباد" }, { "id": 1974, "province_id": 146, "country_id": 103, "name": "Delijan", - "slug": "ir-markazi-delijan" + "slug": "ir-markazi-delijan", + "name_fa": "دلیجان" }, { "id": 1975, @@ -13829,28 +14340,32 @@ "province_id": 146, "country_id": 103, "name": "Farmahin", - "slug": "ir-markazi-farmahin" + "slug": "ir-markazi-farmahin", + "name_fa": "فرمهین" }, { "id": 1977, "province_id": 146, "country_id": 103, "name": "Gharqabad", - "slug": "ir-markazi-gharqabad" + "slug": "ir-markazi-gharqabad", + "name_fa": "غرق آباد" }, { "id": 1978, "province_id": 146, "country_id": 103, "name": "Ghurchi Bashi", - "slug": "ir-markazi-ghurchi-bashi" + "slug": "ir-markazi-ghurchi-bashi", + "name_fa": "قورچی باشی" }, { "id": 1979, "province_id": 146, "country_id": 103, "name": "Hendoudar", - "slug": "ir-markazi-hendoudar" + "slug": "ir-markazi-hendoudar", + "name_fa": "هندودر" }, { "id": 1980, @@ -13878,7 +14393,8 @@ "province_id": 146, "country_id": 103, "name": "Khomein", - "slug": "ir-markazi-khomein" + "slug": "ir-markazi-khomein", + "name_fa": "خمین" }, { "id": 1984, @@ -13892,28 +14408,32 @@ "province_id": 146, "country_id": 103, "name": "Khondab", - "slug": "ir-markazi-khondab" + "slug": "ir-markazi-khondab", + "name_fa": "خنداب" }, { "id": 1986, "province_id": 146, "country_id": 103, "name": "Khoshkrud", - "slug": "ir-markazi-khoshkrud" + "slug": "ir-markazi-khoshkrud", + "name_fa": "خشکرود" }, { "id": 1987, "province_id": 146, "country_id": 103, "name": "Komijan", - "slug": "ir-markazi-komijan" + "slug": "ir-markazi-komijan", + "name_fa": "کمیجان" }, { "id": 1988, "province_id": 146, "country_id": 103, "name": "Mahalat", - "slug": "ir-markazi-mahalat" + "slug": "ir-markazi-mahalat", + "name_fa": "محلات" }, { "id": 1989, @@ -13927,63 +14447,72 @@ "province_id": 146, "country_id": 103, "name": "Mamuniyeh", - "slug": "ir-markazi-mamuniyeh" + "slug": "ir-markazi-mamuniyeh", + "name_fa": "مامونیه" }, { "id": 1991, "province_id": 146, "country_id": 103, "name": "Milajerd", - "slug": "ir-markazi-milajerd" + "slug": "ir-markazi-milajerd", + "name_fa": "میلاجرد" }, { "id": 1992, "province_id": 146, "country_id": 103, "name": "Mohajeran", - "slug": "ir-markazi-mohajeran" + "slug": "ir-markazi-mohajeran", + "name_fa": "مهاجران" }, { "id": 1993, "province_id": 146, "country_id": 103, "name": "Naragh", - "slug": "ir-markazi-naragh" + "slug": "ir-markazi-naragh", + "name_fa": "نراق" }, { "id": 1994, "province_id": 146, "country_id": 103, "name": "Nimvar", - "slug": "ir-markazi-nimvar" + "slug": "ir-markazi-nimvar", + "name_fa": "نیمور" }, { "id": 1995, "province_id": 146, "country_id": 103, "name": "Nowbaran", - "slug": "ir-markazi-nowbaran" + "slug": "ir-markazi-nowbaran", + "name_fa": "نوبران" }, { "id": 1996, "province_id": 146, "country_id": 103, "name": "Parandak", - "slug": "ir-markazi-parandak" + "slug": "ir-markazi-parandak", + "name_fa": "پرندک" }, { "id": 1997, "province_id": 146, "country_id": 103, "name": "Razeqan", - "slug": "ir-markazi-razeqan" + "slug": "ir-markazi-razeqan", + "name_fa": "رازقان" }, { "id": 1998, "province_id": 146, "country_id": 103, "name": "Saroogh", - "slug": "ir-markazi-saroogh" + "slug": "ir-markazi-saroogh", + "name_fa": "ساروق" }, { "id": 1999, @@ -14004,14 +14533,16 @@ "province_id": 146, "country_id": 103, "name": "Shazand", - "slug": "ir-markazi-shazand" + "slug": "ir-markazi-shazand", + "name_fa": "شازند" }, { "id": 2002, "province_id": 146, "country_id": 103, "name": "Tafresh", - "slug": "ir-markazi-tafresh" + "slug": "ir-markazi-tafresh", + "name_fa": "تفرش" }, { "id": 2003, @@ -14032,35 +14563,40 @@ "province_id": 146, "country_id": 103, "name": "Zavieh", - "slug": "ir-markazi-zavieh" + "slug": "ir-markazi-zavieh", + "name_fa": "زاویه" }, { "id": 2006, "province_id": 147, "country_id": 103, "name": "Abbasabad", - "slug": "ir-mazandaran-abbasabad" + "slug": "ir-mazandaran-abbasabad", + "name_fa": "عباس آباد" }, { "id": 2007, "province_id": 147, "country_id": 103, "name": "Alasht", - "slug": "ir-mazandaran-alasht" + "slug": "ir-mazandaran-alasht", + "name_fa": "آلاشت" }, { "id": 2008, "province_id": 147, "country_id": 103, "name": "Amir Kala", - "slug": "ir-mazandaran-amir-kala" + "slug": "ir-mazandaran-amir-kala", + "name_fa": "امیرکلا" }, { "id": 2009, "province_id": 147, "country_id": 103, "name": "Amol", - "slug": "ir-mazandaran-amol" + "slug": "ir-mazandaran-amol", + "name_fa": "آمل" }, { "id": 2010, @@ -14074,21 +14610,24 @@ "province_id": 147, "country_id": 103, "name": "Babol", - "slug": "ir-mazandaran-babol" + "slug": "ir-mazandaran-babol", + "name_fa": "بابل" }, { "id": 2012, "province_id": 147, "country_id": 103, "name": "Babolsar", - "slug": "ir-mazandaran-babolsar" + "slug": "ir-mazandaran-babolsar", + "name_fa": "بابلسر" }, { "id": 2013, "province_id": 147, "country_id": 103, "name": "Bahnamir", - "slug": "ir-mazandaran-bahnamir" + "slug": "ir-mazandaran-bahnamir", + "name_fa": "بهنمیر" }, { "id": 2014, @@ -14102,28 +14641,32 @@ "province_id": 147, "country_id": 103, "name": "Behshahr", - "slug": "ir-mazandaran-behshahr" + "slug": "ir-mazandaran-behshahr", + "name_fa": "بهشهر" }, { "id": 2016, "province_id": 147, "country_id": 103, "name": "Chalus", - "slug": "ir-mazandaran-chalus" + "slug": "ir-mazandaran-chalus", + "name_fa": "چالوس" }, { "id": 2017, "province_id": 147, "country_id": 103, "name": "Chamestan", - "slug": "ir-mazandaran-chamestan" + "slug": "ir-mazandaran-chamestan", + "name_fa": "چمستان" }, { "id": 2018, "province_id": 147, "country_id": 103, "name": "Dabudasht", - "slug": "ir-mazandaran-dabudasht" + "slug": "ir-mazandaran-dabudasht", + "name_fa": "دابودشت" }, { "id": 2019, @@ -14137,7 +14680,8 @@ "province_id": 147, "country_id": 103, "name": "Farim", - "slug": "ir-mazandaran-farim" + "slug": "ir-mazandaran-farim", + "name_fa": "فریم" }, { "id": 2021, @@ -14151,14 +14695,16 @@ "province_id": 147, "country_id": 103, "name": "Galugah", - "slug": "ir-mazandaran-galugah" + "slug": "ir-mazandaran-galugah", + "name_fa": "گلوگاه" }, { "id": 2023, "province_id": 147, "country_id": 103, "name": "Gatab", - "slug": "ir-mazandaran-gatab" + "slug": "ir-mazandaran-gatab", + "name_fa": "گتاب" }, { "id": 2024, @@ -14179,21 +14725,24 @@ "province_id": 147, "country_id": 103, "name": "Izadshahr", - "slug": "ir-mazandaran-izadshahr" + "slug": "ir-mazandaran-izadshahr", + "name_fa": "ایزدشهر" }, { "id": 2027, "province_id": 147, "country_id": 103, "name": "Juybar", - "slug": "ir-mazandaran-juybar" + "slug": "ir-mazandaran-juybar", + "name_fa": "جویبار" }, { "id": 2028, "province_id": 147, "country_id": 103, "name": "Kalar Dasht", - "slug": "ir-mazandaran-kalar-dasht" + "slug": "ir-mazandaran-kalar-dasht", + "name_fa": "کلاردشت" }, { "id": 2029, @@ -14207,7 +14756,8 @@ "province_id": 147, "country_id": 103, "name": "Kelarabad", - "slug": "ir-mazandaran-kelarabad" + "slug": "ir-mazandaran-kelarabad", + "name_fa": "کلارآباد" }, { "id": 2031, @@ -14228,35 +14778,40 @@ "province_id": 147, "country_id": 103, "name": "Khalil Shahr", - "slug": "ir-mazandaran-khalil-shahr" + "slug": "ir-mazandaran-khalil-shahr", + "name_fa": "خلیل شهر" }, { "id": 2034, "province_id": 147, "country_id": 103, "name": "Khorramabad", - "slug": "ir-mazandaran-khorramabad" + "slug": "ir-mazandaran-khorramabad", + "name_fa": "خرم آباد" }, { "id": 2035, "province_id": 147, "country_id": 103, "name": "Khoshroud Pey", - "slug": "ir-mazandaran-khoshroud-pey" + "slug": "ir-mazandaran-khoshroud-pey", + "name_fa": "خوش رودپی" }, { "id": 2036, "province_id": 147, "country_id": 103, "name": "Kia Kola", - "slug": "ir-mazandaran-kia-kola" + "slug": "ir-mazandaran-kia-kola", + "name_fa": "کیاکلا" }, { "id": 2037, "province_id": 147, "country_id": 103, "name": "Kiasar", - "slug": "ir-mazandaran-kiasar" + "slug": "ir-mazandaran-kiasar", + "name_fa": "کیاسر" }, { "id": 2038, @@ -14270,28 +14825,32 @@ "province_id": 147, "country_id": 103, "name": "Kuhi Khil", - "slug": "ir-mazandaran-kuhi-khil" + "slug": "ir-mazandaran-kuhi-khil", + "name_fa": "کوهی خیل" }, { "id": 2040, "province_id": 147, "country_id": 103, "name": "Mahmudabad", - "slug": "ir-mazandaran-mahmudabad" + "slug": "ir-mazandaran-mahmudabad", + "name_fa": "محمودآباد" }, { "id": 2041, "province_id": 147, "country_id": 103, "name": "Marzanabad", - "slug": "ir-mazandaran-marzanabad" + "slug": "ir-mazandaran-marzanabad", + "name_fa": "مرزن آباد" }, { "id": 2042, "province_id": 147, "country_id": 103, "name": "Marzikola", - "slug": "ir-mazandaran-marzikola" + "slug": "ir-mazandaran-marzikola", + "name_fa": "مرزیکلا" }, { "id": 2043, @@ -14305,28 +14864,32 @@ "province_id": 147, "country_id": 103, "name": "Nashtarud", - "slug": "ir-mazandaran-nashtarud" + "slug": "ir-mazandaran-nashtarud", + "name_fa": "نشتارود" }, { "id": 2045, "province_id": 147, "country_id": 103, "name": "Neka", - "slug": "ir-mazandaran-neka" + "slug": "ir-mazandaran-neka", + "name_fa": "نکا" }, { "id": 2046, "province_id": 147, "country_id": 103, "name": "Nowshahr", - "slug": "ir-mazandaran-nowshahr" + "slug": "ir-mazandaran-nowshahr", + "name_fa": "نوشهر" }, { "id": 2047, "province_id": 147, "country_id": 103, "name": "Nur", - "slug": "ir-mazandaran-nur" + "slug": "ir-mazandaran-nur", + "name_fa": "نور" }, { "id": 2048, @@ -14340,7 +14903,8 @@ "province_id": 147, "country_id": 103, "name": "Pol Sefid", - "slug": "ir-mazandaran-pol-sefid" + "slug": "ir-mazandaran-pol-sefid", + "name_fa": "پل سفید" }, { "id": 2050, @@ -14361,42 +14925,48 @@ "province_id": 147, "country_id": 103, "name": "Ramsar", - "slug": "ir-mazandaran-ramsar" + "slug": "ir-mazandaran-ramsar", + "name_fa": "رامسر" }, { "id": 2053, "province_id": 147, "country_id": 103, "name": "Rineh", - "slug": "ir-mazandaran-rineh" + "slug": "ir-mazandaran-rineh", + "name_fa": "رینه" }, { "id": 2054, "province_id": 147, "country_id": 103, "name": "Rostam kola", - "slug": "ir-mazandaran-rostam-kola" + "slug": "ir-mazandaran-rostam-kola", + "name_fa": "رستمکلا" }, { "id": 2055, "province_id": 147, "country_id": 103, "name": "Royan", - "slug": "ir-mazandaran-royan" + "slug": "ir-mazandaran-royan", + "name_fa": "رویان" }, { "id": 2056, "province_id": 147, "country_id": 103, "name": "Salman Shahr", - "slug": "ir-mazandaran-salman-shahr" + "slug": "ir-mazandaran-salman-shahr", + "name_fa": "سلمان شهر" }, { "id": 2057, "province_id": 147, "country_id": 103, "name": "Sari", - "slug": "ir-mazandaran-sari" + "slug": "ir-mazandaran-sari", + "name_fa": "ساری" }, { "id": 2058, @@ -14410,14 +14980,16 @@ "province_id": 147, "country_id": 103, "name": "Shirgah", - "slug": "ir-mazandaran-shirgah" + "slug": "ir-mazandaran-shirgah", + "name_fa": "شیرگاه" }, { "id": 2060, "province_id": 147, "country_id": 103, "name": "Shirud", - "slug": "ir-mazandaran-shirud" + "slug": "ir-mazandaran-shirud", + "name_fa": "شیرود" }, { "id": 2061, @@ -14431,7 +15003,8 @@ "province_id": 147, "country_id": 103, "name": "Sorkh Roud", - "slug": "ir-mazandaran-sorkh-roud" + "slug": "ir-mazandaran-sorkh-roud", + "name_fa": "سرخرود" }, { "id": 2063, @@ -14445,7 +15018,8 @@ "province_id": 147, "country_id": 103, "name": "Tonekabon", - "slug": "ir-mazandaran-tonekabon" + "slug": "ir-mazandaran-tonekabon", + "name_fa": "تنکابن" }, { "id": 2065, @@ -14459,14 +15033,16 @@ "province_id": 147, "country_id": 103, "name": "Zirab", - "slug": "ir-mazandaran-zirab" + "slug": "ir-mazandaran-zirab", + "name_fa": "زیرآب" }, { "id": 2067, "province_id": 148, "country_id": 103, "name": "Ashkhaneh", - "slug": "ir-north-khorasan-ashkhaneh" + "slug": "ir-north-khorasan-ashkhaneh", + "name_fa": "آشخانه" }, { "id": 2068, @@ -14480,7 +15056,8 @@ "province_id": 148, "country_id": 103, "name": "Bojnurd", - "slug": "ir-north-khorasan-bojnurd" + "slug": "ir-north-khorasan-bojnurd", + "name_fa": "بجنورد" }, { "id": 2070, @@ -14508,7 +15085,8 @@ "province_id": 148, "country_id": 103, "name": "Farooj", - "slug": "ir-north-khorasan-farooj" + "slug": "ir-north-khorasan-farooj", + "name_fa": "فاروج" }, { "id": 2074, @@ -14522,14 +15100,16 @@ "province_id": 148, "country_id": 103, "name": "Garmeh", - "slug": "ir-north-khorasan-garmeh" + "slug": "ir-north-khorasan-garmeh", + "name_fa": "گرمه" }, { "id": 2076, "province_id": 148, "country_id": 103, "name": "Hesar-e Garmkhan", - "slug": "ir-north-khorasan-hesar-e-garmkhan" + "slug": "ir-north-khorasan-hesar-e-garmkhan", + "name_fa": "حصارگرمخان" }, { "id": 2077, @@ -14543,14 +15123,16 @@ "province_id": 148, "country_id": 103, "name": "Jajarm", - "slug": "ir-north-khorasan-jajarm" + "slug": "ir-north-khorasan-jajarm", + "name_fa": "جاجرم" }, { "id": 2079, "province_id": 148, "country_id": 103, "name": "Lujali", - "slug": "ir-north-khorasan-lujali" + "slug": "ir-north-khorasan-lujali", + "name_fa": "لوجلی" }, { "id": 2080, @@ -14564,7 +15146,8 @@ "province_id": 148, "country_id": 103, "name": "Pish Ghaleh", - "slug": "ir-north-khorasan-pish-ghaleh" + "slug": "ir-north-khorasan-pish-ghaleh", + "name_fa": "پیش قلعه" }, { "id": 2082, @@ -14585,42 +15168,48 @@ "province_id": 148, "country_id": 103, "name": "Raz", - "slug": "ir-north-khorasan-raz" + "slug": "ir-north-khorasan-raz", + "name_fa": "راز" }, { "id": 2085, "province_id": 148, "country_id": 103, "name": "Safiabad", - "slug": "ir-north-khorasan-safiabad" + "slug": "ir-north-khorasan-safiabad", + "name_fa": "صفی آباد" }, { "id": 2086, "province_id": 148, "country_id": 103, "name": "Sankhast", - "slug": "ir-north-khorasan-sankhast" + "slug": "ir-north-khorasan-sankhast", + "name_fa": "سنخواست" }, { "id": 2087, "province_id": 148, "country_id": 103, "name": "Shirvan", - "slug": "ir-north-khorasan-shirvan" + "slug": "ir-north-khorasan-shirvan", + "name_fa": "شیروان" }, { "id": 2088, "province_id": 148, "country_id": 103, "name": "Shoqan", - "slug": "ir-north-khorasan-shoqan" + "slug": "ir-north-khorasan-shoqan", + "name_fa": "شوقان" }, { "id": 2089, "province_id": 148, "country_id": 103, "name": "Titkanlu", - "slug": "ir-north-khorasan-titkanlu" + "slug": "ir-north-khorasan-titkanlu", + "name_fa": "تیتکانلو" }, { "id": 2090, @@ -14634,7 +15223,8 @@ "province_id": 149, "country_id": 103, "name": "Abgarm", - "slug": "ir-qazvin-abgarm" + "slug": "ir-qazvin-abgarm", + "name_fa": "آبگرم" }, { "id": 2092, @@ -14655,14 +15245,16 @@ "province_id": 149, "country_id": 103, "name": "Alvand", - "slug": "ir-qazvin-alvand" + "slug": "ir-qazvin-alvand", + "name_fa": "الوند" }, { "id": 2095, "province_id": 149, "country_id": 103, "name": "Ardagh", - "slug": "ir-qazvin-ardagh" + "slug": "ir-qazvin-ardagh", + "name_fa": "ارداق" }, { "id": 2096, @@ -14676,21 +15268,24 @@ "province_id": 149, "country_id": 103, "name": "Bidestan", - "slug": "ir-qazvin-bidestan" + "slug": "ir-qazvin-bidestan", + "name_fa": "بیدستان" }, { "id": 2098, "province_id": 149, "country_id": 103, "name": "Buin Zahra", - "slug": "ir-qazvin-buin-zahra" + "slug": "ir-qazvin-buin-zahra", + "name_fa": "بوئین زهرا" }, { "id": 2099, "province_id": 149, "country_id": 103, "name": "Danesfahan", - "slug": "ir-qazvin-danesfahan" + "slug": "ir-qazvin-danesfahan", + "name_fa": "دانسفهان" }, { "id": 2100, @@ -14711,28 +15306,32 @@ "province_id": 149, "country_id": 103, "name": "Khakali", - "slug": "ir-qazvin-khakali" + "slug": "ir-qazvin-khakali", + "name_fa": "خاکعلی" }, { "id": 2103, "province_id": 149, "country_id": 103, "name": "Khorramdasht", - "slug": "ir-qazvin-khorramdasht" + "slug": "ir-qazvin-khorramdasht", + "name_fa": "خرمدشت" }, { "id": 2104, "province_id": 149, "country_id": 103, "name": "Kouhin", - "slug": "ir-qazvin-kouhin" + "slug": "ir-qazvin-kouhin", + "name_fa": "کوهین" }, { "id": 2105, "province_id": 149, "country_id": 103, "name": "Mahmood Abad nemooneh", - "slug": "ir-qazvin-mahmood-abad-nemooneh" + "slug": "ir-qazvin-mahmood-abad-nemooneh", + "name_fa": "محمودآباد نمونه" }, { "id": 2106, @@ -14753,7 +15352,8 @@ "province_id": 149, "country_id": 103, "name": "Narjeh", - "slug": "ir-qazvin-narjeh" + "slug": "ir-qazvin-narjeh", + "name_fa": "نرجه" }, { "id": 2109, @@ -14767,21 +15367,24 @@ "province_id": 149, "country_id": 103, "name": "Razmian", - "slug": "ir-qazvin-razmian" + "slug": "ir-qazvin-razmian", + "name_fa": "رازمیان" }, { "id": 2111, "province_id": 149, "country_id": 103, "name": "Sagz Abad", - "slug": "ir-qazvin-sagz-abad" + "slug": "ir-qazvin-sagz-abad", + "name_fa": "سگزآباد" }, { "id": 2112, "province_id": 149, "country_id": 103, "name": "Shal", - "slug": "ir-qazvin-shal" + "slug": "ir-qazvin-shal", + "name_fa": "شال" }, { "id": 2113, @@ -14795,35 +15398,40 @@ "province_id": 149, "country_id": 103, "name": "Sirdan", - "slug": "ir-qazvin-sirdan" + "slug": "ir-qazvin-sirdan", + "name_fa": "سیردان" }, { "id": 2115, "province_id": 149, "country_id": 103, "name": "Takestan", - "slug": "ir-qazvin-takestan" + "slug": "ir-qazvin-takestan", + "name_fa": "تاکستان" }, { "id": 2116, "province_id": 149, "country_id": 103, "name": "Ziaabad", - "slug": "ir-qazvin-ziaabad" + "slug": "ir-qazvin-ziaabad", + "name_fa": "ضیاآباد" }, { "id": 2117, "province_id": 150, "country_id": 103, "name": "Dastjerd", - "slug": "ir-qom-dastjerd" + "slug": "ir-qom-dastjerd", + "name_fa": "دستجرد" }, { "id": 2118, "province_id": 150, "country_id": 103, "name": "Jafarie", - "slug": "ir-qom-jafarie" + "slug": "ir-qom-jafarie", + "name_fa": "جعفریه" }, { "id": 2119, @@ -14851,161 +15459,184 @@ "province_id": 150, "country_id": 103, "name": "Salafchegan", - "slug": "ir-qom-salafchegan" + "slug": "ir-qom-salafchegan", + "name_fa": "سلفچگان" }, { "id": 2123, "province_id": 151, "country_id": 103, "name": "Ahmad-abad-e-Solat", - "slug": "ir-razavi-khorasan-ahmad-abad-e-solat" + "slug": "ir-razavi-khorasan-ahmad-abad-e-solat", + "name_fa": "احمدآباد صولت" }, { "id": 2124, "province_id": 151, "country_id": 103, "name": "Anabad", - "slug": "ir-razavi-khorasan-anabad" + "slug": "ir-razavi-khorasan-anabad", + "name_fa": "انابد" }, { "id": 2125, "province_id": 151, "country_id": 103, "name": "Bajestan", - "slug": "ir-razavi-khorasan-bajestan" + "slug": "ir-razavi-khorasan-bajestan", + "name_fa": "بجستان" }, { "id": 2126, "province_id": 151, "country_id": 103, "name": "Bajgiran", - "slug": "ir-razavi-khorasan-bajgiran" + "slug": "ir-razavi-khorasan-bajgiran", + "name_fa": "باجگیران" }, { "id": 2127, "province_id": 151, "country_id": 103, "name": "Bakharz", - "slug": "ir-razavi-khorasan-bakharz" + "slug": "ir-razavi-khorasan-bakharz", + "name_fa": "باخرز" }, { "id": 2128, "province_id": 151, "country_id": 103, "name": "Bar", - "slug": "ir-razavi-khorasan-bar" + "slug": "ir-razavi-khorasan-bar", + "name_fa": "بار" }, { "id": 2129, "province_id": 151, "country_id": 103, "name": "Bardaskan", - "slug": "ir-razavi-khorasan-bardaskan" + "slug": "ir-razavi-khorasan-bardaskan", + "name_fa": "بردسکن" }, { "id": 2130, "province_id": 151, "country_id": 103, "name": "Bayg", - "slug": "ir-razavi-khorasan-bayg" + "slug": "ir-razavi-khorasan-bayg", + "name_fa": "بایگ" }, { "id": 2131, "province_id": 151, "country_id": 103, "name": "Beydokht", - "slug": "ir-razavi-khorasan-beydokht" + "slug": "ir-razavi-khorasan-beydokht", + "name_fa": "بیدخت" }, { "id": 2132, "province_id": 151, "country_id": 103, "name": "Binalud", - "slug": "ir-razavi-khorasan-binalud" + "slug": "ir-razavi-khorasan-binalud", + "name_fa": "بینالود" }, { "id": 2133, "province_id": 151, "country_id": 103, "name": "Chakaneh", - "slug": "ir-razavi-khorasan-chakaneh" + "slug": "ir-razavi-khorasan-chakaneh", + "name_fa": "چکنه" }, { "id": 2134, "province_id": 151, "country_id": 103, "name": "Chapeshlu", - "slug": "ir-razavi-khorasan-chapeshlu" + "slug": "ir-razavi-khorasan-chapeshlu", + "name_fa": "چاپشلو" }, { "id": 2135, "province_id": 151, "country_id": 103, "name": "Chenaran", - "slug": "ir-razavi-khorasan-chenaran" + "slug": "ir-razavi-khorasan-chenaran", + "name_fa": "چناران" }, { "id": 2136, "province_id": 151, "country_id": 103, "name": "Dargaz", - "slug": "ir-razavi-khorasan-dargaz" + "slug": "ir-razavi-khorasan-dargaz", + "name_fa": "درگز" }, { "id": 2137, "province_id": 151, "country_id": 103, "name": "Darrod", - "slug": "ir-razavi-khorasan-darrod" + "slug": "ir-razavi-khorasan-darrod", + "name_fa": "در رود" }, { "id": 2138, "province_id": 151, "country_id": 103, "name": "Davarzan", - "slug": "ir-razavi-khorasan-davarzan" + "slug": "ir-razavi-khorasan-davarzan", + "name_fa": "داورزن" }, { "id": 2139, "province_id": 151, "country_id": 103, "name": "Dowlat Abad", - "slug": "ir-razavi-khorasan-dowlat-abad" + "slug": "ir-razavi-khorasan-dowlat-abad", + "name_fa": "دولت آباد" }, { "id": 2140, "province_id": 151, "country_id": 103, "name": "Eshqabad", - "slug": "ir-razavi-khorasan-eshqabad" + "slug": "ir-razavi-khorasan-eshqabad", + "name_fa": "عشق آباد" }, { "id": 2141, "province_id": 151, "country_id": 103, "name": "Farhadgerd", - "slug": "ir-razavi-khorasan-farhadgerd" + "slug": "ir-razavi-khorasan-farhadgerd", + "name_fa": "فرهادگرد" }, { "id": 2142, "province_id": 151, "country_id": 103, "name": "Fariman", - "slug": "ir-razavi-khorasan-fariman" + "slug": "ir-razavi-khorasan-fariman", + "name_fa": "فریمان" }, { "id": 2143, "province_id": 151, "country_id": 103, "name": "Feyz Abad", - "slug": "ir-razavi-khorasan-feyz-abad" + "slug": "ir-razavi-khorasan-feyz-abad", + "name_fa": "فیض آباد" }, { "id": 2144, "province_id": 151, "country_id": 103, "name": "Firouzeh", - "slug": "ir-razavi-khorasan-firouzeh" + "slug": "ir-razavi-khorasan-firouzeh", + "name_fa": "فیروزه" }, { "id": 2145, @@ -15019,49 +15650,56 @@ "province_id": 151, "country_id": 103, "name": "Ghadamgah", - "slug": "ir-razavi-khorasan-ghadamgah" + "slug": "ir-razavi-khorasan-ghadamgah", + "name_fa": "قدمگاه" }, { "id": 2147, "province_id": 151, "country_id": 103, "name": "Golbahar", - "slug": "ir-razavi-khorasan-golbahar" + "slug": "ir-razavi-khorasan-golbahar", + "name_fa": "گلبهار" }, { "id": 2148, "province_id": 151, "country_id": 103, "name": "Golmakan", - "slug": "ir-razavi-khorasan-golmakan" + "slug": "ir-razavi-khorasan-golmakan", + "name_fa": "گلمکان" }, { "id": 2149, "province_id": 151, "country_id": 103, "name": "Gonabad", - "slug": "ir-razavi-khorasan-gonabad" + "slug": "ir-razavi-khorasan-gonabad", + "name_fa": "گناباد" }, { "id": 2150, "province_id": 151, "country_id": 103, "name": "Hemmatabad", - "slug": "ir-razavi-khorasan-hemmatabad" + "slug": "ir-razavi-khorasan-hemmatabad", + "name_fa": "همت آباد" }, { "id": 2151, "province_id": 151, "country_id": 103, "name": "Jangal", - "slug": "ir-razavi-khorasan-jangal" + "slug": "ir-razavi-khorasan-jangal", + "name_fa": "جنگل" }, { "id": 2152, "province_id": 151, "country_id": 103, "name": "Joghatay", - "slug": "ir-razavi-khorasan-joghatay" + "slug": "ir-razavi-khorasan-joghatay", + "name_fa": "جغتای" }, { "id": 2153, @@ -15075,49 +15713,56 @@ "province_id": 151, "country_id": 103, "name": "Kadkan", - "slug": "ir-razavi-khorasan-kadkan" + "slug": "ir-razavi-khorasan-kadkan", + "name_fa": "کدکن" }, { "id": 2155, "province_id": 151, "country_id": 103, "name": "Kakhk", - "slug": "ir-razavi-khorasan-kakhk" + "slug": "ir-razavi-khorasan-kakhk", + "name_fa": "کاخک" }, { "id": 2156, "province_id": 151, "country_id": 103, "name": "Kalat", - "slug": "ir-razavi-khorasan-kalat" + "slug": "ir-razavi-khorasan-kalat", + "name_fa": "کلات" }, { "id": 2157, "province_id": 151, "country_id": 103, "name": "Kariz", - "slug": "ir-razavi-khorasan-kariz" + "slug": "ir-razavi-khorasan-kariz", + "name_fa": "کاریز" }, { "id": 2158, "province_id": 151, "country_id": 103, "name": "Kashmar", - "slug": "ir-razavi-khorasan-kashmar" + "slug": "ir-razavi-khorasan-kashmar", + "name_fa": "کاشمر" }, { "id": 2159, "province_id": 151, "country_id": 103, "name": "Khaf", - "slug": "ir-razavi-khorasan-khaf" + "slug": "ir-razavi-khorasan-khaf", + "name_fa": "خواف" }, { "id": 2160, "province_id": 151, "country_id": 103, "name": "Khalil Abad", - "slug": "ir-razavi-khorasan-khalil-abad" + "slug": "ir-razavi-khorasan-khalil-abad", + "name_fa": "خلیل آباد" }, { "id": 2161, @@ -15145,14 +15790,16 @@ "province_id": 151, "country_id": 103, "name": "Kondor", - "slug": "ir-razavi-khorasan-kondor" + "slug": "ir-razavi-khorasan-kondor", + "name_fa": "کندر" }, { "id": 2165, "province_id": 151, "country_id": 103, "name": "Lotfabad", - "slug": "ir-razavi-khorasan-lotfabad" + "slug": "ir-razavi-khorasan-lotfabad", + "name_fa": "لطف آباد" }, { "id": 2166, @@ -15166,7 +15813,8 @@ "province_id": 151, "country_id": 103, "name": "Mashhad", - "slug": "ir-razavi-khorasan-mashhad" + "slug": "ir-razavi-khorasan-mashhad", + "name_fa": "مشهد" }, { "id": 2168, @@ -15180,70 +15828,80 @@ "province_id": 151, "country_id": 103, "name": "Mazdavand", - "slug": "ir-razavi-khorasan-mazdavand" + "slug": "ir-razavi-khorasan-mazdavand", + "name_fa": "مزدآوند" }, { "id": 2170, "province_id": 151, "country_id": 103, "name": "Molkabad", - "slug": "ir-razavi-khorasan-molkabad" + "slug": "ir-razavi-khorasan-molkabad", + "name_fa": "ملک آباد" }, { "id": 2171, "province_id": 151, "country_id": 103, "name": "Nashtifan", - "slug": "ir-razavi-khorasan-nashtifan" + "slug": "ir-razavi-khorasan-nashtifan", + "name_fa": "نشتیفان" }, { "id": 2172, "province_id": 151, "country_id": 103, "name": "Nasrabad", - "slug": "ir-razavi-khorasan-nasrabad" + "slug": "ir-razavi-khorasan-nasrabad", + "name_fa": "نصرآباد" }, { "id": 2173, "province_id": 151, "country_id": 103, "name": "Neghab", - "slug": "ir-razavi-khorasan-neghab" + "slug": "ir-razavi-khorasan-neghab", + "name_fa": "نقاب" }, { "id": 2174, "province_id": 151, "country_id": 103, "name": "Neyshabur", - "slug": "ir-razavi-khorasan-neyshabur" + "slug": "ir-razavi-khorasan-neyshabur", + "name_fa": "نیشابور" }, { "id": 2175, "province_id": 151, "country_id": 103, "name": "Nil Shahr", - "slug": "ir-razavi-khorasan-nil-shahr" + "slug": "ir-razavi-khorasan-nil-shahr", + "name_fa": "نیل شهر" }, { "id": 2176, "province_id": 151, "country_id": 103, "name": "Nokhandan", - "slug": "ir-razavi-khorasan-nokhandan" + "slug": "ir-razavi-khorasan-nokhandan", + "name_fa": "نوخندان" }, { "id": 2177, "province_id": 151, "country_id": 103, "name": "Qalandarabad", - "slug": "ir-razavi-khorasan-qalandarabad" + "slug": "ir-razavi-khorasan-qalandarabad", + "name_fa": "قلندرآباد" }, { "id": 2178, "province_id": 151, "country_id": 103, "name": "Qasemabad", - "slug": "ir-razavi-khorasan-qasemabad" + "slug": "ir-razavi-khorasan-qasemabad", + "name_fa": "قاسم آباد" }, { "id": 2179, @@ -15271,49 +15929,56 @@ "province_id": 151, "country_id": 103, "name": "Rivash", - "slug": "ir-razavi-khorasan-rivash" + "slug": "ir-razavi-khorasan-rivash", + "name_fa": "ریوش" }, { "id": 2183, "province_id": 151, "country_id": 103, "name": "Robat-e-Sang", - "slug": "ir-razavi-khorasan-robat-e-sang" + "slug": "ir-razavi-khorasan-robat-e-sang", + "name_fa": "رباط سنگ" }, { "id": 2184, "province_id": 151, "country_id": 103, "name": "Roshtkhvar", - "slug": "ir-razavi-khorasan-roshtkhvar" + "slug": "ir-razavi-khorasan-roshtkhvar", + "name_fa": "رشتخوار" }, { "id": 2185, "province_id": 151, "country_id": 103, "name": "Rudab", - "slug": "ir-razavi-khorasan-rudab" + "slug": "ir-razavi-khorasan-rudab", + "name_fa": "روداب" }, { "id": 2186, "province_id": 151, "country_id": 103, "name": "Sabzevar", - "slug": "ir-razavi-khorasan-sabzevar" + "slug": "ir-razavi-khorasan-sabzevar", + "name_fa": "سبزوار" }, { "id": 2187, "province_id": 151, "country_id": 103, "name": "Salami", - "slug": "ir-razavi-khorasan-salami" + "slug": "ir-razavi-khorasan-salami", + "name_fa": "سلامی" }, { "id": 2188, "province_id": 151, "country_id": 103, "name": "Saleh Abad", - "slug": "ir-razavi-khorasan-saleh-abad" + "slug": "ir-razavi-khorasan-saleh-abad", + "name_fa": "صالح آباد" }, { "id": 2189, @@ -15327,70 +15992,80 @@ "province_id": 151, "country_id": 103, "name": "Sangan", - "slug": "ir-razavi-khorasan-sangan" + "slug": "ir-razavi-khorasan-sangan", + "name_fa": "سنگان" }, { "id": 2191, "province_id": 151, "country_id": 103, "name": "Sarakhs", - "slug": "ir-razavi-khorasan-sarakhs" + "slug": "ir-razavi-khorasan-sarakhs", + "name_fa": "سرخس" }, { "id": 2192, "province_id": 151, "country_id": 103, "name": "Sefid Sang", - "slug": "ir-razavi-khorasan-sefid-sang" + "slug": "ir-razavi-khorasan-sefid-sang", + "name_fa": "سفیدسنگ" }, { "id": 2193, "province_id": 151, "country_id": 103, "name": "Shadmehr", - "slug": "ir-razavi-khorasan-shadmehr" + "slug": "ir-razavi-khorasan-shadmehr", + "name_fa": "شادمهر" }, { "id": 2194, "province_id": 151, "country_id": 103, "name": "Shahrabad", - "slug": "ir-razavi-khorasan-shahrabad" + "slug": "ir-razavi-khorasan-shahrabad", + "name_fa": "شهرآباد" }, { "id": 2195, "province_id": 151, "country_id": 103, "name": "Shahrezu", - "slug": "ir-razavi-khorasan-shahrezu" + "slug": "ir-razavi-khorasan-shahrezu", + "name_fa": "شهرزو" }, { "id": 2196, "province_id": 151, "country_id": 103, "name": "Shandiz", - "slug": "ir-razavi-khorasan-shandiz" + "slug": "ir-razavi-khorasan-shandiz", + "name_fa": "شاندیز" }, { "id": 2197, "province_id": 151, "country_id": 103, "name": "Sheshtamad", - "slug": "ir-razavi-khorasan-sheshtamad" + "slug": "ir-razavi-khorasan-sheshtamad", + "name_fa": "ششتمد" }, { "id": 2198, "province_id": 151, "country_id": 103, "name": "Soltanabad", - "slug": "ir-razavi-khorasan-soltanabad" + "slug": "ir-razavi-khorasan-soltanabad", + "name_fa": "سلطان آباد" }, { "id": 2199, "province_id": 151, "country_id": 103, "name": "Taybad", - "slug": "ir-razavi-khorasan-taybad" + "slug": "ir-razavi-khorasan-taybad", + "name_fa": "تایباد" }, { "id": 2200, @@ -15404,14 +16079,16 @@ "province_id": 151, "country_id": 103, "name": "Torbat-e-Jam", - "slug": "ir-razavi-khorasan-torbat-e-jam" + "slug": "ir-razavi-khorasan-torbat-e-jam", + "name_fa": "تربت جام" }, { "id": 2202, "province_id": 151, "country_id": 103, "name": "Torghabeh", - "slug": "ir-razavi-khorasan-torghabeh" + "slug": "ir-razavi-khorasan-torghabeh", + "name_fa": "طرقبه" }, { "id": 2203, @@ -15432,49 +16109,56 @@ "province_id": 152, "country_id": 103, "name": "Amiriyeh", - "slug": "ir-semnan-amiriyeh" + "slug": "ir-semnan-amiriyeh", + "name_fa": "امیریه" }, { "id": 2206, "province_id": 152, "country_id": 103, "name": "Aradan", - "slug": "ir-semnan-aradan" + "slug": "ir-semnan-aradan", + "name_fa": "آرادان" }, { "id": 2207, "province_id": 152, "country_id": 103, "name": "Bastam", - "slug": "ir-semnan-bastam" + "slug": "ir-semnan-bastam", + "name_fa": "بسطام" }, { "id": 2208, "province_id": 152, "country_id": 103, "name": "Biyārjomand", - "slug": "ir-semnan-biy-rjomand" + "slug": "ir-semnan-biy-rjomand", + "name_fa": "بیارجمند" }, { "id": 2209, "province_id": 152, "country_id": 103, "name": "Damghan", - "slug": "ir-semnan-damghan" + "slug": "ir-semnan-damghan", + "name_fa": "دامغان" }, { "id": 2210, "province_id": 152, "country_id": 103, "name": "Darjazin", - "slug": "ir-semnan-darjazin" + "slug": "ir-semnan-darjazin", + "name_fa": "درجزین" }, { "id": 2211, "province_id": 152, "country_id": 103, "name": "Dibaj", - "slug": "ir-semnan-dibaj" + "slug": "ir-semnan-dibaj", + "name_fa": "دیباج" }, { "id": 2212, @@ -15488,14 +16172,16 @@ "province_id": 152, "country_id": 103, "name": "Garmsar", - "slug": "ir-semnan-garmsar" + "slug": "ir-semnan-garmsar", + "name_fa": "گرمسار" }, { "id": 2214, "province_id": 152, "country_id": 103, "name": "kalāte Khij", - "slug": "ir-semnan-kal-te-khij" + "slug": "ir-semnan-kal-te-khij", + "name_fa": "کلاته خیج" }, { "id": 2215, @@ -15516,7 +16202,8 @@ "province_id": 152, "country_id": 103, "name": "Mahdi Shahr", - "slug": "ir-semnan-mahdi-shahr" + "slug": "ir-semnan-mahdi-shahr", + "name_fa": "مهدی شهر" }, { "id": 2218, @@ -15544,21 +16231,24 @@ "province_id": 152, "country_id": 103, "name": "Semnan", - "slug": "ir-semnan-semnan" + "slug": "ir-semnan-semnan", + "name_fa": "سمنان" }, { "id": 2222, "province_id": 152, "country_id": 103, "name": "Shahmirzad", - "slug": "ir-semnan-shahmirzad" + "slug": "ir-semnan-shahmirzad", + "name_fa": "شهمیرزاد" }, { "id": 2223, "province_id": 152, "country_id": 103, "name": "Shahroud", - "slug": "ir-semnan-shahroud" + "slug": "ir-semnan-shahroud", + "name_fa": "شاهرود" }, { "id": 2224, @@ -15572,49 +16262,56 @@ "province_id": 152, "country_id": 103, "name": "Sorkheh", - "slug": "ir-semnan-sorkheh" + "slug": "ir-semnan-sorkheh", + "name_fa": "سرخه" }, { "id": 2226, "province_id": 153, "country_id": 103, "name": "Adimi", - "slug": "ir-sistan-and-baluchestan-adimi" + "slug": "ir-sistan-and-baluchestan-adimi", + "name_fa": "ادیمی" }, { "id": 2227, "province_id": 153, "country_id": 103, "name": "Bampour", - "slug": "ir-sistan-and-baluchestan-bampour" + "slug": "ir-sistan-and-baluchestan-bampour", + "name_fa": "بمپور" }, { "id": 2228, "province_id": 153, "country_id": 103, "name": "Bazmān", - "slug": "ir-sistan-and-baluchestan-bazm-n" + "slug": "ir-sistan-and-baluchestan-bazm-n", + "name_fa": "بزمان" }, { "id": 2229, "province_id": 153, "country_id": 103, "name": "Bent", - "slug": "ir-sistan-and-baluchestan-bent" + "slug": "ir-sistan-and-baluchestan-bent", + "name_fa": "بنت" }, { "id": 2230, "province_id": 153, "country_id": 103, "name": "Bonjār", - "slug": "ir-sistan-and-baluchestan-bonj-r" + "slug": "ir-sistan-and-baluchestan-bonj-r", + "name_fa": "بنجار" }, { "id": 2231, "province_id": 153, "country_id": 103, "name": "Chabahar", - "slug": "ir-sistan-and-baluchestan-chabahar" + "slug": "ir-sistan-and-baluchestan-chabahar", + "name_fa": "چابهار" }, { "id": 2232, @@ -15642,7 +16339,8 @@ "province_id": 153, "country_id": 103, "name": "Fanouj", - "slug": "ir-sistan-and-baluchestan-fanouj" + "slug": "ir-sistan-and-baluchestan-fanouj", + "name_fa": "فنوج" }, { "id": 2236, @@ -15656,14 +16354,16 @@ "province_id": 153, "country_id": 103, "name": "Golmorti", - "slug": "ir-sistan-and-baluchestan-golmorti" + "slug": "ir-sistan-and-baluchestan-golmorti", + "name_fa": "گلمورتی" }, { "id": 2238, "province_id": 153, "country_id": 103, "name": "Gosht", - "slug": "ir-sistan-and-baluchestan-gosht" + "slug": "ir-sistan-and-baluchestan-gosht", + "name_fa": "گشت" }, { "id": 2239, @@ -15677,7 +16377,8 @@ "province_id": 153, "country_id": 103, "name": "Hidouj", - "slug": "ir-sistan-and-baluchestan-hidouj" + "slug": "ir-sistan-and-baluchestan-hidouj", + "name_fa": "هیدوچ" }, { "id": 2241, @@ -15691,7 +16392,8 @@ "province_id": 153, "country_id": 103, "name": "Iranshahr", - "slug": "ir-sistan-and-baluchestan-iranshahr" + "slug": "ir-sistan-and-baluchestan-iranshahr", + "name_fa": "ایرانشهر" }, { "id": 2243, @@ -15705,14 +16407,16 @@ "province_id": 153, "country_id": 103, "name": "Khash", - "slug": "ir-sistan-and-baluchestan-khash" + "slug": "ir-sistan-and-baluchestan-khash", + "name_fa": "خاش" }, { "id": 2245, "province_id": 153, "country_id": 103, "name": "Konarak", - "slug": "ir-sistan-and-baluchestan-konarak" + "slug": "ir-sistan-and-baluchestan-konarak", + "name_fa": "کنارک" }, { "id": 2246, @@ -15726,21 +16430,24 @@ "province_id": 153, "country_id": 103, "name": "Mirjaveh", - "slug": "ir-sistan-and-baluchestan-mirjaveh" + "slug": "ir-sistan-and-baluchestan-mirjaveh", + "name_fa": "میرجاوه" }, { "id": 2248, "province_id": 153, "country_id": 103, "name": "Mohamadan", - "slug": "ir-sistan-and-baluchestan-mohamadan" + "slug": "ir-sistan-and-baluchestan-mohamadan", + "name_fa": "محمدان" }, { "id": 2249, "province_id": 153, "country_id": 103, "name": "Mohammadābād", - "slug": "ir-sistan-and-baluchestan-mohammad-b-d" + "slug": "ir-sistan-and-baluchestan-mohammad-b-d", + "name_fa": "محمدآباد" }, { "id": 2250, @@ -15754,14 +16461,16 @@ "province_id": 153, "country_id": 103, "name": "Negour", - "slug": "ir-sistan-and-baluchestan-negour" + "slug": "ir-sistan-and-baluchestan-negour", + "name_fa": "نگور" }, { "id": 2252, "province_id": 153, "country_id": 103, "name": "Nikshahr", - "slug": "ir-sistan-and-baluchestan-nikshahr" + "slug": "ir-sistan-and-baluchestan-nikshahr", + "name_fa": "نیک شهر" }, { "id": 2253, @@ -15775,21 +16484,24 @@ "province_id": 153, "country_id": 103, "name": "Nosrat Abad", - "slug": "ir-sistan-and-baluchestan-nosrat-abad" + "slug": "ir-sistan-and-baluchestan-nosrat-abad", + "name_fa": "نصرت آباد" }, { "id": 2255, "province_id": 153, "country_id": 103, "name": "Noukābād", - "slug": "ir-sistan-and-baluchestan-nouk-b-d" + "slug": "ir-sistan-and-baluchestan-nouk-b-d", + "name_fa": "نوک آباد" }, { "id": 2256, "province_id": 153, "country_id": 103, "name": "Pishin", - "slug": "ir-sistan-and-baluchestan-pishin" + "slug": "ir-sistan-and-baluchestan-pishin", + "name_fa": "پیشین" }, { "id": 2257, @@ -15803,21 +16515,24 @@ "province_id": 153, "country_id": 103, "name": "Rāsk", - "slug": "ir-sistan-and-baluchestan-r-sk" + "slug": "ir-sistan-and-baluchestan-r-sk", + "name_fa": "راسک" }, { "id": 2259, "province_id": 153, "country_id": 103, "name": "Saravan", - "slug": "ir-sistan-and-baluchestan-saravan" + "slug": "ir-sistan-and-baluchestan-saravan", + "name_fa": "سراوان" }, { "id": 2260, "province_id": 153, "country_id": 103, "name": "Sarbaz", - "slug": "ir-sistan-and-baluchestan-sarbaz" + "slug": "ir-sistan-and-baluchestan-sarbaz", + "name_fa": "سرباز" }, { "id": 2261, @@ -15831,28 +16546,32 @@ "province_id": 153, "country_id": 103, "name": "Sirkān", - "slug": "ir-sistan-and-baluchestan-sirk-n" + "slug": "ir-sistan-and-baluchestan-sirk-n", + "name_fa": "سیرکان" }, { "id": 2263, "province_id": 153, "country_id": 103, "name": "Suran", - "slug": "ir-sistan-and-baluchestan-suran" + "slug": "ir-sistan-and-baluchestan-suran", + "name_fa": "سوران" }, { "id": 2264, "province_id": 153, "country_id": 103, "name": "Zabol", - "slug": "ir-sistan-and-baluchestan-zabol" + "slug": "ir-sistan-and-baluchestan-zabol", + "name_fa": "زابل" }, { "id": 2265, "province_id": 153, "country_id": 103, "name": "Zaboli", - "slug": "ir-sistan-and-baluchestan-zaboli" + "slug": "ir-sistan-and-baluchestan-zaboli", + "name_fa": "زابلی" }, { "id": 2266, @@ -15866,7 +16585,8 @@ "province_id": 153, "country_id": 103, "name": "Zahedan", - "slug": "ir-sistan-and-baluchestan-zahedan" + "slug": "ir-sistan-and-baluchestan-zahedan", + "name_fa": "زاهدان" }, { "id": 2268, @@ -15887,14 +16607,16 @@ "province_id": 154, "country_id": 103, "name": "Arian Shahr", - "slug": "ir-south-khorasan-arian-shahr" + "slug": "ir-south-khorasan-arian-shahr", + "name_fa": "آرین شهر" }, { "id": 2271, "province_id": 154, "country_id": 103, "name": "Asadieh", - "slug": "ir-south-khorasan-asadieh" + "slug": "ir-south-khorasan-asadieh", + "name_fa": "اسدیه" }, { "id": 2272, @@ -15908,7 +16630,8 @@ "province_id": 154, "country_id": 103, "name": "Birjand", - "slug": "ir-south-khorasan-birjand" + "slug": "ir-south-khorasan-birjand", + "name_fa": "بیرجند" }, { "id": 2274, @@ -15964,14 +16687,16 @@ "province_id": 154, "country_id": 103, "name": "Eslamiyeh", - "slug": "ir-south-khorasan-eslamiyeh" + "slug": "ir-south-khorasan-eslamiyeh", + "name_fa": "اسلامیه" }, { "id": 2282, "province_id": 154, "country_id": 103, "name": "Ferdows", - "slug": "ir-south-khorasan-ferdows" + "slug": "ir-south-khorasan-ferdows", + "name_fa": "فردوس" }, { "id": 2283, @@ -15985,35 +16710,40 @@ "province_id": 154, "country_id": 103, "name": "Ghayen", - "slug": "ir-south-khorasan-ghayen" + "slug": "ir-south-khorasan-ghayen", + "name_fa": "قاین" }, { "id": 2285, "province_id": 154, "country_id": 103, "name": "Ghohestan", - "slug": "ir-south-khorasan-ghohestan" + "slug": "ir-south-khorasan-ghohestan", + "name_fa": "قهستان" }, { "id": 2286, "province_id": 154, "country_id": 103, "name": "Hajiabad", - "slug": "ir-south-khorasan-hajiabad" + "slug": "ir-south-khorasan-hajiabad", + "name_fa": "حاجی آباد" }, { "id": 2287, "province_id": 154, "country_id": 103, "name": "Khezri Dashtebayaz", - "slug": "ir-south-khorasan-khezri-dashtebayaz" + "slug": "ir-south-khorasan-khezri-dashtebayaz", + "name_fa": "خضری دشت بیاض" }, { "id": 2288, "province_id": 154, "country_id": 103, "name": "Khusf", - "slug": "ir-south-khorasan-khusf" + "slug": "ir-south-khorasan-khusf", + "name_fa": "خوسف" }, { "id": 2289, @@ -16027,21 +16757,24 @@ "province_id": 154, "country_id": 103, "name": "Mud", - "slug": "ir-south-khorasan-mud" + "slug": "ir-south-khorasan-mud", + "name_fa": "مود" }, { "id": 2291, "province_id": 154, "country_id": 103, "name": "Nehbandan", - "slug": "ir-south-khorasan-nehbandan" + "slug": "ir-south-khorasan-nehbandan", + "name_fa": "نهبندان" }, { "id": 2292, "province_id": 154, "country_id": 103, "name": "Nimbolook", - "slug": "ir-south-khorasan-nimbolook" + "slug": "ir-south-khorasan-nimbolook", + "name_fa": "نیمبلوک" }, { "id": 2293, @@ -16055,7 +16788,8 @@ "province_id": 154, "country_id": 103, "name": "Sarayan", - "slug": "ir-south-khorasan-sarayan" + "slug": "ir-south-khorasan-sarayan", + "name_fa": "سرایان" }, { "id": 2295, @@ -16069,7 +16803,8 @@ "province_id": 154, "country_id": 103, "name": "Sarbisheh", - "slug": "ir-south-khorasan-sarbisheh" + "slug": "ir-south-khorasan-sarbisheh", + "name_fa": "سربیشه" }, { "id": 2297, @@ -16083,7 +16818,8 @@ "province_id": 154, "country_id": 103, "name": "Shusf", - "slug": "ir-south-khorasan-shusf" + "slug": "ir-south-khorasan-shusf", + "name_fa": "شوسف" }, { "id": 2299, @@ -16111,21 +16847,24 @@ "province_id": 154, "country_id": 103, "name": "Zohaan", - "slug": "ir-south-khorasan-zohaan" + "slug": "ir-south-khorasan-zohaan", + "name_fa": "زهان" }, { "id": 2303, "province_id": 155, "country_id": 103, "name": "Abali", - "slug": "ir-tehran-abali" + "slug": "ir-tehran-abali", + "name_fa": "آبعلی" }, { "id": 2304, "province_id": 155, "country_id": 103, "name": "Absard", - "slug": "ir-tehran-absard" + "slug": "ir-tehran-absard", + "name_fa": "آبسرد" }, { "id": 2305, @@ -16139,28 +16878,32 @@ "province_id": 155, "country_id": 103, "name": "Andisheh", - "slug": "ir-tehran-andisheh" + "slug": "ir-tehran-andisheh", + "name_fa": "اندیشه" }, { "id": 2307, "province_id": 155, "country_id": 103, "name": "Arjmand", - "slug": "ir-tehran-arjmand" + "slug": "ir-tehran-arjmand", + "name_fa": "ارجمند" }, { "id": 2308, "province_id": 155, "country_id": 103, "name": "Baghershahr", - "slug": "ir-tehran-baghershahr" + "slug": "ir-tehran-baghershahr", + "name_fa": "باقرشهر" }, { "id": 2309, "province_id": 155, "country_id": 103, "name": "Baghestan", - "slug": "ir-tehran-baghestan" + "slug": "ir-tehran-baghestan", + "name_fa": "باغستان" }, { "id": 2310, @@ -16181,14 +16924,16 @@ "province_id": 155, "country_id": 103, "name": "Chahar Dangeh", - "slug": "ir-tehran-chahar-dangeh" + "slug": "ir-tehran-chahar-dangeh", + "name_fa": "چهاردانگه" }, { "id": 2313, "province_id": 155, "country_id": 103, "name": "Damavand", - "slug": "ir-tehran-damavand" + "slug": "ir-tehran-damavand", + "name_fa": "دماوند" }, { "id": 2314, @@ -16202,21 +16947,24 @@ "province_id": 155, "country_id": 103, "name": "Eslamshahr", - "slug": "ir-tehran-eslamshahr" + "slug": "ir-tehran-eslamshahr", + "name_fa": "اسلامشهر" }, { "id": 2316, "province_id": 155, "country_id": 103, "name": "Fasham", - "slug": "ir-tehran-fasham" + "slug": "ir-tehran-fasham", + "name_fa": "فشم" }, { "id": 2317, "province_id": 155, "country_id": 103, "name": "Ferdosiye", - "slug": "ir-tehran-ferdosiye" + "slug": "ir-tehran-ferdosiye", + "name_fa": "فردوسیه" }, { "id": 2318, @@ -16230,63 +16978,72 @@ "province_id": 155, "country_id": 103, "name": "Firuzkuh", - "slug": "ir-tehran-firuzkuh" + "slug": "ir-tehran-firuzkuh", + "name_fa": "فیروزکوه" }, { "id": 2320, "province_id": 155, "country_id": 103, "name": "Golestan", - "slug": "ir-tehran-golestan" + "slug": "ir-tehran-golestan", + "name_fa": "گلستان" }, { "id": 2321, "province_id": 155, "country_id": 103, "name": "Hasanabad", - "slug": "ir-tehran-hasanabad" + "slug": "ir-tehran-hasanabad", + "name_fa": "حسن آباد" }, { "id": 2322, "province_id": 155, "country_id": 103, "name": "Javadabad", - "slug": "ir-tehran-javadabad" + "slug": "ir-tehran-javadabad", + "name_fa": "جوادآباد" }, { "id": 2323, "province_id": 155, "country_id": 103, "name": "Kahrizak", - "slug": "ir-tehran-kahrizak" + "slug": "ir-tehran-kahrizak", + "name_fa": "کهریزک" }, { "id": 2324, "province_id": 155, "country_id": 103, "name": "Kilan", - "slug": "ir-tehran-kilan" + "slug": "ir-tehran-kilan", + "name_fa": "کیلان" }, { "id": 2325, "province_id": 155, "country_id": 103, "name": "Lavasan", - "slug": "ir-tehran-lavasan" + "slug": "ir-tehran-lavasan", + "name_fa": "لواسان" }, { "id": 2326, "province_id": 155, "country_id": 103, "name": "Malard", - "slug": "ir-tehran-malard" + "slug": "ir-tehran-malard", + "name_fa": "ملارد" }, { "id": 2327, "province_id": 155, "country_id": 103, "name": "Nasimshahr", - "slug": "ir-tehran-nasimshahr" + "slug": "ir-tehran-nasimshahr", + "name_fa": "نسیم شهر" }, { "id": 2328, @@ -16300,28 +17057,32 @@ "province_id": 155, "country_id": 103, "name": "Pakdasht", - "slug": "ir-tehran-pakdasht" + "slug": "ir-tehran-pakdasht", + "name_fa": "پاکدشت" }, { "id": 2330, "province_id": 155, "country_id": 103, "name": "Parand", - "slug": "ir-tehran-parand" + "slug": "ir-tehran-parand", + "name_fa": "پرند" }, { "id": 2331, "province_id": 155, "country_id": 103, "name": "Pardis", - "slug": "ir-tehran-pardis" + "slug": "ir-tehran-pardis", + "name_fa": "پردیس" }, { "id": 2332, "province_id": 155, "country_id": 103, "name": "Pishva", - "slug": "ir-tehran-pishva" + "slug": "ir-tehran-pishva", + "name_fa": "پیشوا" }, { "id": 2333, @@ -16349,7 +17110,8 @@ "province_id": 155, "country_id": 103, "name": "Robat Karim", - "slug": "ir-tehran-robat-karim" + "slug": "ir-tehran-robat-karim", + "name_fa": "رباط کریم" }, { "id": 2337, @@ -16363,14 +17125,16 @@ "province_id": 155, "country_id": 103, "name": "Sabashahr", - "slug": "ir-tehran-sabashahr" + "slug": "ir-tehran-sabashahr", + "name_fa": "صباشهر" }, { "id": 2339, "province_id": 155, "country_id": 103, "name": "Safadasht", - "slug": "ir-tehran-safadasht" + "slug": "ir-tehran-safadasht", + "name_fa": "صفادشت" }, { "id": 2340, @@ -16384,7 +17148,8 @@ "province_id": 155, "country_id": 103, "name": "Shahedshahr", - "slug": "ir-tehran-shahedshahr" + "slug": "ir-tehran-shahedshahr", + "name_fa": "شاهدشهر" }, { "id": 2342, @@ -16405,14 +17170,16 @@ "province_id": 155, "country_id": 103, "name": "Shahriar", - "slug": "ir-tehran-shahriar" + "slug": "ir-tehran-shahriar", + "name_fa": "شهریار" }, { "id": 2345, "province_id": 155, "country_id": 103, "name": "Sharifabad", - "slug": "ir-tehran-sharifabad" + "slug": "ir-tehran-sharifabad", + "name_fa": "شریف آباد" }, { "id": 2346, @@ -16426,7 +17193,8 @@ "province_id": 155, "country_id": 103, "name": "Shemshak", - "slug": "ir-tehran-shemshak" + "slug": "ir-tehran-shemshak", + "name_fa": "شمشک" }, { "id": 2348, @@ -16447,28 +17215,32 @@ "province_id": 155, "country_id": 103, "name": "Taleb abad", - "slug": "ir-tehran-taleb-abad" + "slug": "ir-tehran-taleb-abad", + "name_fa": "صالح آباد" }, { "id": 2351, "province_id": 155, "country_id": 103, "name": "Tehran", - "slug": "ir-tehran-tehran" + "slug": "ir-tehran-tehran", + "name_fa": "تهران" }, { "id": 2352, "province_id": 155, "country_id": 103, "name": "Vahidieh", - "slug": "ir-tehran-vahidieh" + "slug": "ir-tehran-vahidieh", + "name_fa": "وحیدیه" }, { "id": 2353, "province_id": 155, "country_id": 103, "name": "Varamin", - "slug": "ir-tehran-varamin" + "slug": "ir-tehran-varamin", + "name_fa": "ورامین" }, { "id": 2354, @@ -16489,14 +17261,16 @@ "province_id": 156, "country_id": 103, "name": "Bazargan", - "slug": "ir-west-azarbaijan-bazargan" + "slug": "ir-west-azarbaijan-bazargan", + "name_fa": "بازرگان" }, { "id": 2357, "province_id": 156, "country_id": 103, "name": "Bukan", - "slug": "ir-west-azarbaijan-bukan" + "slug": "ir-west-azarbaijan-bukan", + "name_fa": "بوکان" }, { "id": 2358, @@ -16510,7 +17284,8 @@ "province_id": 156, "country_id": 103, "name": "Charburj", - "slug": "ir-west-azarbaijan-charburj" + "slug": "ir-west-azarbaijan-charburj", + "name_fa": "چهاربرج" }, { "id": 2360, @@ -16524,7 +17299,8 @@ "province_id": 156, "country_id": 103, "name": "Dizaj Diz", - "slug": "ir-west-azarbaijan-dizaj-diz" + "slug": "ir-west-azarbaijan-dizaj-diz", + "name_fa": "دیزج دیز" }, { "id": 2362, @@ -16545,14 +17321,16 @@ "province_id": 156, "country_id": 103, "name": "Gerd Kashaneh", - "slug": "ir-west-azarbaijan-gerd-kashaneh" + "slug": "ir-west-azarbaijan-gerd-kashaneh", + "name_fa": "گردکشانه" }, { "id": 2365, "province_id": 156, "country_id": 103, "name": "Keshavarz", - "slug": "ir-west-azarbaijan-keshavarz" + "slug": "ir-west-azarbaijan-keshavarz", + "name_fa": "کشاورز" }, { "id": 2366, @@ -16573,28 +17351,32 @@ "province_id": 156, "country_id": 103, "name": "Khoy", - "slug": "ir-west-azarbaijan-khoy" + "slug": "ir-west-azarbaijan-khoy", + "name_fa": "خوی" }, { "id": 2369, "province_id": 156, "country_id": 103, "name": "Mahabad", - "slug": "ir-west-azarbaijan-mahabad" + "slug": "ir-west-azarbaijan-mahabad", + "name_fa": "مهاباد" }, { "id": 2370, "province_id": 156, "country_id": 103, "name": "Mahmoodabad", - "slug": "ir-west-azarbaijan-mahmoodabad" + "slug": "ir-west-azarbaijan-mahmoodabad", + "name_fa": "محمودآباد" }, { "id": 2371, "province_id": 156, "country_id": 103, "name": "Maku", - "slug": "ir-west-azarbaijan-maku" + "slug": "ir-west-azarbaijan-maku", + "name_fa": "ماکو" }, { "id": 2372, @@ -16608,14 +17390,16 @@ "province_id": 156, "country_id": 103, "name": "Miandoab", - "slug": "ir-west-azarbaijan-miandoab" + "slug": "ir-west-azarbaijan-miandoab", + "name_fa": "میاندوآب" }, { "id": 2374, "province_id": 156, "country_id": 103, "name": "Mirabad", - "slug": "ir-west-azarbaijan-mirabad" + "slug": "ir-west-azarbaijan-mirabad", + "name_fa": "میرآباد" }, { "id": 2375, @@ -16629,7 +17413,8 @@ "province_id": 156, "country_id": 103, "name": "Nalous", - "slug": "ir-west-azarbaijan-nalous" + "slug": "ir-west-azarbaijan-nalous", + "name_fa": "نالوس" }, { "id": 2377, @@ -16650,7 +17435,8 @@ "province_id": 156, "country_id": 103, "name": "Noshin", - "slug": "ir-west-azarbaijan-noshin" + "slug": "ir-west-azarbaijan-noshin", + "name_fa": "نوشین" }, { "id": 2380, @@ -16671,14 +17457,16 @@ "province_id": 156, "country_id": 103, "name": "Piranshahr", - "slug": "ir-west-azarbaijan-piranshahr" + "slug": "ir-west-azarbaijan-piranshahr", + "name_fa": "پیرانشهر" }, { "id": 2383, "province_id": 156, "country_id": 103, "name": "Poldasht", - "slug": "ir-west-azarbaijan-poldasht" + "slug": "ir-west-azarbaijan-poldasht", + "name_fa": "پلدشت" }, { "id": 2384, @@ -16699,7 +17487,8 @@ "province_id": 156, "country_id": 103, "name": "Qushchi", - "slug": "ir-west-azarbaijan-qushchi" + "slug": "ir-west-azarbaijan-qushchi", + "name_fa": "قوشچی" }, { "id": 2387, @@ -16713,14 +17502,16 @@ "province_id": 156, "country_id": 103, "name": "Salmas", - "slug": "ir-west-azarbaijan-salmas" + "slug": "ir-west-azarbaijan-salmas", + "name_fa": "سلماس" }, { "id": 2389, "province_id": 156, "country_id": 103, "name": "Sardasht", - "slug": "ir-west-azarbaijan-sardasht" + "slug": "ir-west-azarbaijan-sardasht", + "name_fa": "سردشت" }, { "id": 2390, @@ -16734,49 +17525,56 @@ "province_id": 156, "country_id": 103, "name": "Shahin Dej", - "slug": "ir-west-azarbaijan-shahin-dej" + "slug": "ir-west-azarbaijan-shahin-dej", + "name_fa": "شاهین دژ" }, { "id": 2392, "province_id": 156, "country_id": 103, "name": "Showt", - "slug": "ir-west-azarbaijan-showt" + "slug": "ir-west-azarbaijan-showt", + "name_fa": "شوط" }, { "id": 2393, "province_id": 156, "country_id": 103, "name": "Siah Cheshmeh", - "slug": "ir-west-azarbaijan-siah-cheshmeh" + "slug": "ir-west-azarbaijan-siah-cheshmeh", + "name_fa": "سیه چشمه" }, { "id": 2394, "province_id": 156, "country_id": 103, "name": "Silvana", - "slug": "ir-west-azarbaijan-silvana" + "slug": "ir-west-azarbaijan-silvana", + "name_fa": "سیلوانه" }, { "id": 2395, "province_id": 156, "country_id": 103, "name": "Simmineh", - "slug": "ir-west-azarbaijan-simmineh" + "slug": "ir-west-azarbaijan-simmineh", + "name_fa": "سیمینه" }, { "id": 2396, "province_id": 156, "country_id": 103, "name": "Takab", - "slug": "ir-west-azarbaijan-takab" + "slug": "ir-west-azarbaijan-takab", + "name_fa": "تکاب" }, { "id": 2397, "province_id": 156, "country_id": 103, "name": "Taze Shar", - "slug": "ir-west-azarbaijan-taze-shar" + "slug": "ir-west-azarbaijan-taze-shar", + "name_fa": "تازه شهر" }, { "id": 2398, @@ -16797,7 +17595,8 @@ "province_id": 157, "country_id": 103, "name": "Abarkooh", - "slug": "ir-yazd-abarkooh" + "slug": "ir-yazd-abarkooh", + "name_fa": "ابرکوه" }, { "id": 2401, @@ -16811,7 +17610,8 @@ "province_id": 157, "country_id": 103, "name": "Ahmadabad", - "slug": "ir-yazd-ahmadabad" + "slug": "ir-yazd-ahmadabad", + "name_fa": "احمدآباد" }, { "id": 2403, @@ -16825,21 +17625,24 @@ "province_id": 157, "country_id": 103, "name": "Ardakan", - "slug": "ir-yazd-ardakan" + "slug": "ir-yazd-ardakan", + "name_fa": "اردکان" }, { "id": 2405, "province_id": 157, "country_id": 103, "name": "Ashkezar", - "slug": "ir-yazd-ashkezar" + "slug": "ir-yazd-ashkezar", + "name_fa": "اشکذر" }, { "id": 2406, "province_id": 157, "country_id": 103, "name": "Bafgh", - "slug": "ir-yazd-bafgh" + "slug": "ir-yazd-bafgh", + "name_fa": "بافق" }, { "id": 2407, @@ -16860,7 +17663,8 @@ "province_id": 157, "country_id": 103, "name": "Bahabad", - "slug": "ir-yazd-bahabad" + "slug": "ir-yazd-bahabad", + "name_fa": "بهاباد" }, { "id": 2410, @@ -16874,14 +17678,16 @@ "province_id": 157, "country_id": 103, "name": "Hamidia", - "slug": "ir-yazd-hamidia" + "slug": "ir-yazd-hamidia", + "name_fa": "حمیدیا" }, { "id": 2412, "province_id": 157, "country_id": 103, "name": "Harat", - "slug": "ir-yazd-harat" + "slug": "ir-yazd-harat", + "name_fa": "هرات" }, { "id": 2413, @@ -16902,14 +17708,16 @@ "province_id": 157, "country_id": 103, "name": "Khezrabad", - "slug": "ir-yazd-khezrabad" + "slug": "ir-yazd-khezrabad", + "name_fa": "خضرآباد" }, { "id": 2416, "province_id": 157, "country_id": 103, "name": "Mahriz", - "slug": "ir-yazd-mahriz" + "slug": "ir-yazd-mahriz", + "name_fa": "مهریز" }, { "id": 2417, @@ -16923,7 +17731,8 @@ "province_id": 157, "country_id": 103, "name": "Mehrdasht", - "slug": "ir-yazd-mehrdasht" + "slug": "ir-yazd-mehrdasht", + "name_fa": "مهردشت" }, { "id": 2419, @@ -16944,21 +17753,24 @@ "province_id": 157, "country_id": 103, "name": "Nir", - "slug": "ir-yazd-nir" + "slug": "ir-yazd-nir", + "name_fa": "نیر" }, { "id": 2422, "province_id": 157, "country_id": 103, "name": "Nodoushan", - "slug": "ir-yazd-nodoushan" + "slug": "ir-yazd-nodoushan", + "name_fa": "ندوشن" }, { "id": 2423, "province_id": 157, "country_id": 103, "name": "Shahedieh", - "slug": "ir-yazd-shahedieh" + "slug": "ir-yazd-shahedieh", + "name_fa": "شاهدیه" }, { "id": 2424, @@ -16972,7 +17784,8 @@ "province_id": 157, "country_id": 103, "name": "Taft", - "slug": "ir-yazd-taft" + "slug": "ir-yazd-taft", + "name_fa": "تفت" }, { "id": 2426, @@ -16986,21 +17799,24 @@ "province_id": 157, "country_id": 103, "name": "Zarach", - "slug": "ir-yazd-zarach" + "slug": "ir-yazd-zarach", + "name_fa": "زارچ" }, { "id": 2428, "province_id": 158, "country_id": 103, "name": "Abbar", - "slug": "ir-zanjan-abbar" + "slug": "ir-zanjan-abbar", + "name_fa": "آب بر" }, { "id": 2429, "province_id": 158, "country_id": 103, "name": "Abhar", - "slug": "ir-zanjan-abhar" + "slug": "ir-zanjan-abhar", + "name_fa": "ابهر" }, { "id": 2430, @@ -17014,7 +17830,8 @@ "province_id": 158, "country_id": 103, "name": "Armaqankhaneh", - "slug": "ir-zanjan-armaqankhaneh" + "slug": "ir-zanjan-armaqankhaneh", + "name_fa": "ارمغان خانه" }, { "id": 2432, @@ -17028,14 +17845,16 @@ "province_id": 158, "country_id": 103, "name": "Dandi", - "slug": "ir-zanjan-dandi" + "slug": "ir-zanjan-dandi", + "name_fa": "دندی" }, { "id": 2434, "province_id": 158, "country_id": 103, "name": "Garmaab", - "slug": "ir-zanjan-garmaab" + "slug": "ir-zanjan-garmaab", + "name_fa": "گرماب" }, { "id": 2435, @@ -17049,7 +17868,8 @@ "province_id": 158, "country_id": 103, "name": "Hidaj", - "slug": "ir-zanjan-hidaj" + "slug": "ir-zanjan-hidaj", + "name_fa": "هیدج" }, { "id": 2437, @@ -17084,7 +17904,8 @@ "province_id": 158, "country_id": 103, "name": "Mahneshan", - "slug": "ir-zanjan-mahneshan" + "slug": "ir-zanjan-mahneshan", + "name_fa": "ماه نشان" }, { "id": 2442, @@ -17126,14 +17947,16 @@ "province_id": 158, "country_id": 103, "name": "Sojas", - "slug": "ir-zanjan-sojas" + "slug": "ir-zanjan-sojas", + "name_fa": "سجاس" }, { "id": 2448, "province_id": 158, "country_id": 103, "name": "Soltanieh", - "slug": "ir-zanjan-soltanieh" + "slug": "ir-zanjan-soltanieh", + "name_fa": "سلطانیه" }, { "id": 2449, @@ -17147,21 +17970,24 @@ "province_id": 158, "country_id": 103, "name": "Zanjan", - "slug": "ir-zanjan-zanjan" + "slug": "ir-zanjan-zanjan", + "name_fa": "زنجان" }, { "id": 2451, "province_id": 158, "country_id": 103, "name": "Zarrin Abad", - "slug": "ir-zanjan-zarrin-abad" + "slug": "ir-zanjan-zarrin-abad", + "name_fa": "زرین آباد" }, { "id": 2452, "province_id": 158, "country_id": 103, "name": "Zarrin Rood", - "slug": "ir-zanjan-zarrin-rood" + "slug": "ir-zanjan-zarrin-rood", + "name_fa": "زرین رود" }, { "id": 2453, diff --git a/index.js b/index.js index c6bc2e1..1767143 100644 --- a/index.js +++ b/index.js @@ -637,6 +637,7 @@ io.on('connection', (socket) => { // Free Project require('./services/AdvertisingCron') require('./services/ProjectCron') +require('./services/OfferReplyCron').setIo(io) require('./services/OrderLifecycleCron').setIo(io) require('./services/BookingLifecycleCron').setIo(io) require('./services/BookingRepeatExpiryCron').setIo(io) diff --git a/middlewares/auditLogger.js b/middlewares/auditLogger.js new file mode 100644 index 0000000..f78bec7 --- /dev/null +++ b/middlewares/auditLogger.js @@ -0,0 +1,35 @@ +const AuditLog = require('../models/AuditLogModel') + +const SENSITIVE_KEYS = new Set(['password', 'new_password', 'token', 'otp']) +const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']) + +function sanitizeBody(body) { + if (!body || typeof body !== 'object') return null + const clone = {} + for (const [key, value] of Object.entries(body)) { + clone[key] = SENSITIVE_KEYS.has(key) ? '[hidden]' : value + } + return clone +} + +// باید بعد از adminAuth بیاد (به req.admin نیاز داره). فقط متدهای نوشتاری رو ثبت +// می‌کنه — GET یعنی فقط دیدن، نه یه «کار انجام‌شده» که نیاز به تاریخچه داشته باشه +module.exports = function auditLogger(req, res, next) { + if (!WRITE_METHODS.has(req.method)) return next() + + res.on('finish', () => { + if (!req.admin) return + AuditLog.create({ + adminId: req.admin._id, + adminUserName: req.admin.user_name, + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + body: sanitizeBody(req.body) + }).catch((err) => { + console.error('[auditLogger] failed to write audit log:', err.message) + }) + }) + + next() +} diff --git a/middlewares/requirePermission.js b/middlewares/requirePermission.js new file mode 100644 index 0000000..a219947 --- /dev/null +++ b/middlewares/requirePermission.js @@ -0,0 +1,19 @@ +// باید بعد از adminAuth بیاد (به req.admin نیاز داره). super_admin همیشه رد می‌شه؛ +// staff فقط اگه key توی permissions خودش باشه +module.exports = function requirePermission(key) { + return (req, res, next) => { + const admin = req.admin + if (!admin) { + return res.status(401).send({ status: 'error', code: 401, message: 'Access Denied' }) + } + if (admin.role === 'super_admin') return next() + if (Array.isArray(admin.permissions) && admin.permissions.includes(key)) { + return next() + } + return res.status(403).send({ + status: 'error', + code: 403, + message: 'شما دسترسی لازم برای این بخش را ندارید' + }) + } +} diff --git a/middlewares/requireSuperAdmin.js b/middlewares/requireSuperAdmin.js new file mode 100644 index 0000000..132a642 --- /dev/null +++ b/middlewares/requireSuperAdmin.js @@ -0,0 +1,13 @@ +// مدیریت کاربران پنل (ساخت/حذف/تغییرِ دسترسیِ ادمین‌های دیگه) همیشه فقط برای +// super_admin مجازه — حتی اگه یه‌جوری کلید 'user-management' به یه staff داده بشه، +// این جلوی افزایشِ خودسرانه‌ی دسترسی (privilege escalation) رو می‌گیره +module.exports = function requireSuperAdmin(req, res, next) { + if (req.admin?.role !== 'super_admin') { + return res.status(403).send({ + status: 'error', + code: 403, + message: 'فقط سوپرادمین به این بخش دسترسی دارد' + }) + } + next() +} diff --git a/models/AdminModel.js b/models/AdminModel.js index 83a358b..68f3a47 100644 --- a/models/AdminModel.js +++ b/models/AdminModel.js @@ -71,6 +71,23 @@ const adminSchema = new mongoose.Schema({ last_online: { type: Date, // ذخیره تاریخ و زمان آخرین بازدید default: Date.now // تنظیم تاریخ به تاریخ فعلی + }, + // super_admin: به همه‌چیز دسترسی داره (بدون نیاز به لیست permissions). staff: فقط به + // مواردی که توی permissions هست. پیش‌فرض super_admin تا ادمین‌های از قبل موجود بعد + // از این تغییر دسترسیشون رو از دست ندن + role: { + type: String, + enum: ['super_admin', 'staff'], + default: 'super_admin' + }, + permissions: { + type: [String], + default: [] + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Admin', + default: null } }) diff --git a/models/AnalyticsEventModel.js b/models/AnalyticsEventModel.js new file mode 100644 index 0000000..7945c68 --- /dev/null +++ b/models/AnalyticsEventModel.js @@ -0,0 +1,66 @@ +const mongoose = require('mongoose') + +// رویدادهای خام رفتار کاربر (بازدید صفحه، کلیک، شروع/پایان نشست) — برای بخش +// «آنالیتیکس» پنل. هر رکورد سبک نگه داشته می‌شه چون حجم بالایی داره؛ تجمیع/گزارش‌گیری +// توی کنترلرهای panel/analytics انجام می‌شه، نه اینجا +const AnalyticsEventSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + default: null, + index: true + }, + sessionId: { + type: String, + required: true, + index: true + }, + type: { + type: String, + enum: ['session_start', 'page_view', 'click'], + required: true, + index: true + }, + // page_view/click: مسیر صفحه‌ای که رویداد توش رخ داده + path: { + type: String, + default: null + }, + // فقط page_view: چند میلی‌ثانیه کاربر روی این صفحه مونده (موقع رفتن به صفحه‌ی + // بعدی یا بستن تب محاسبه و ارسال می‌شه) + durationMs: { + type: Number, + default: null + }, + // فقط click: شناسه‌ی قابل‌خوندنِ عنصر کلیک‌شده (data-analytics-id یا برچسب/متن آن) + label: { + type: String, + default: null + }, + platform: { + type: String, + enum: ['web', 'android', 'ios'], + required: true, + index: true + }, + device: { + type: String, + default: null + }, + browser: { + type: String, + default: null + }, + os: { + type: String, + default: null + } + }, + { timestamps: true } +) + +AnalyticsEventSchema.index({ createdAt: -1 }) +AnalyticsEventSchema.index({ type: 1, createdAt: -1 }) + +module.exports = mongoose.model('AnalyticsEvent', AnalyticsEventSchema) diff --git a/models/AuditLogModel.js b/models/AuditLogModel.js new file mode 100644 index 0000000..992b9c0 --- /dev/null +++ b/models/AuditLogModel.js @@ -0,0 +1,44 @@ +const mongoose = require('mongoose') + +// تاریخچه‌ی سیستم — هر عملِ نوشتاری (POST/PUT/PATCH/DELETE) که یه ادمین توی پنل انجام +// می‌ده خودکار اینجا ثبت می‌شه (middlewares/auditLogger.js)، بدون نیاز به لاگ دستی +// توی تک‌تکِ کنترلرها +const AuditLogSchema = new mongoose.Schema( + { + adminId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Admin', + required: true, + index: true + }, + adminUserName: { + type: String, + default: null + }, + method: { + type: String, + required: true + }, + path: { + type: String, + required: true, + index: true + }, + statusCode: { + type: Number, + default: null + }, + // خلاصه‌ی بدنه‌ی درخواست (پسورد/توکن حذف می‌شه) — برای اینکه معلوم باشه دقیقاً + // چه تغییری داده شده + body: { + type: mongoose.Schema.Types.Mixed, + default: null + } + }, + { timestamps: true } +) + +AuditLogSchema.index({ createdAt: -1 }) +AuditLogSchema.index({ adminId: 1, createdAt: -1 }) + +module.exports = mongoose.model('AuditLog', AuditLogSchema) diff --git a/models/OfferModel.js b/models/OfferModel.js index 525f485..28e29c6 100644 --- a/models/OfferModel.js +++ b/models/OfferModel.js @@ -23,7 +23,7 @@ const offerSchema = new mongoose.Schema({ ref: 'Payment', default: null }, - // سهم ۳۰٪ گیرنده که فقط با پاسخ‌گویی ظرف ۲۴ ساعت به اولین پیام دایرکت واریز می‌شود + // سهم ۳۰٪ گیرنده که فقط با پاسخ‌گویی ظرف ۱۲ ساعت به اولین پیام دایرکت واریز می‌شود — pendingCommissionAmount: { type: Number, default: 0 @@ -43,6 +43,13 @@ const offerSchema = new mongoose.Schema({ commissionCredited: { type: Boolean, default: false + }, + // وقتی گیرنده ظرف مهلت مقرر به اولین پیام دایرکت پاسخ نداد، سهم ۳۰٪ به‌جای گیرنده + // به کیف پول فرستنده (کسی که پرداخت کرده) برمی‌گردد — این فلگ از بازگشتِ دوباره + // در اجراهای بعدیِ cron جلوگیری می‌کند + commissionRefunded: { + type: Boolean, + default: false } }) diff --git a/models/StoryModel.js b/models/StoryModel.js index 1044ad4..5e5a89a 100644 --- a/models/StoryModel.js +++ b/models/StoryModel.js @@ -5,11 +5,21 @@ const timestamp = require('mongoose-timestamp') // type مشخص می‌کنه اورلی متنیه یا لینکِ یکی از موارد قابل‌لینک (پکیج/پروژه/بیلبورد/فروشگاه/ // کالا/رزرو، همون‌هایی که توی ثبت پست هم قابل‌لینک‌ان) const overlaySchema = new mongoose.Schema({ - type: { type: String, enum: ['text', 'link'], default: 'text' }, + type: { type: String, enum: ['text', 'link', 'mention', 'hashtag', 'location', 'sticker'], default: 'text' }, + // text: متن آزاد (type:'text') | متن تگ بدون # (type:'hashtag') | نام مکان (type:'location') | + // خودِ ایموجی (type:'sticker') text: { type: String, default: '' }, x: { type: Number, default: 0.5 }, y: { type: Number, default: 0.5 }, font_size: { type: Number, default: 0.06 }, + // فقط برای type:'text' — کدوم واریانت فونتِ برندِ اپ (Vazir/IRANSansX) استفاده بشه؛ + // چون اپ فعلاً فقط یک خانواده‌فونت با وزن‌های مختلف داره (نه چند تایپ‌فیس واقعیِ جدا مثل + // اینستاگرام)، این همون گزینه‌های واقعاً موجودیه + font_family: { + type: String, + enum: ['light', 'regular', 'medium', 'bold', 'black'], + default: 'bold' + }, color: { type: String, default: '#ffffff' }, background: { type: String, default: '' }, rotation: { type: Number, default: 0 }, @@ -17,7 +27,10 @@ const overlaySchema = new mongoose.Schema({ // فقط برای type:'link' link_kind: { type: String, default: null }, link_id: { type: String, default: null }, - link_title: { type: String, default: null } + link_title: { type: String, default: null }, + // فقط برای type:'mention' + mention_user_id: { type: String, default: null }, + mention_username: { type: String, default: null } }, { _id: false }) const storySchema = new mongoose.Schema({ diff --git a/models/UserModel.js b/models/UserModel.js index 83274e2..6acab1c 100644 --- a/models/UserModel.js +++ b/models/UserModel.js @@ -258,7 +258,159 @@ const userSchema = new mongoose.Schema({ }, marital_status: { type: String, - enum: ['single', 'divorced'], + enum: ['single', 'divorced', 'widowed', 'not_in_relationship', 'prefer_not_to_say'], + required: false, + default: null + }, + relationship_goal: { + type: [{ + type: String, + enum: [ + 'friendship', + 'serious_partner', + 'marriage_intro', + 'emotional_relationship', + 'travel_event_companion', + 'social_networking', + 'not_sure' + ] + }], + required: false, + default: [] + }, + seeking_gender: { + type: String, + enum: ['female', 'male', 'both', 'not_important'], + required: false, + default: null + }, + desired_age_range: { + type: String, + enum: ['18_24', '25_30', '31_35', '36_40', '41_50', 'over_50', 'not_important'], + required: false, + default: null + }, + personality_traits: { + type: [{ + type: String, + enum: [ + 'kind', + 'loyal', + 'funny', + 'calm', + 'energetic', + 'responsible', + 'ambitious', + 'emotional', + 'logical', + 'patient', + 'honest', + 'romantic', + 'independent', + 'family_oriented', + 'adventurous', + 'introvert', + 'extrovert' + ] + }], + required: false, + default: [] + }, + lifestyle: { + type: [{ + type: String, + enum: [ + 'active_healthy', + 'homebody_calm', + 'social_outgoing', + 'busy_career', + 'traveler', + 'night_owl', + 'early_riser', + 'minimalist', + 'luxury_lover' + ] + }], + required: false, + default: [] + }, + interests: { + type: [{ + type: String, + enum: [ + 'movies_series', + 'music', + 'books_reading', + 'sports', + 'cooking', + 'photography', + 'travel', + 'nature', + 'gaming', + 'art_painting', + 'fashion', + 'pets', + 'dancing', + 'language_learning' + ] + }], + required: false, + default: [] + }, + values: { + type: [{ + type: String, + enum: [ + 'family', + 'career_growth', + 'religion_spirituality', + 'personal_freedom', + 'financial_stability', + 'health_fitness', + 'personal_growth', + 'excitement_new_experiences' + ] + }], + required: false, + default: [] + }, + relationship_view: { + type: String, + enum: [ + 'traditional', + 'modern', + 'long_term', + 'friendship_first', + 'marriage_soon', + 'friendship_only_for_now' + ], + required: false, + default: null + }, + geo_preference: { + type: String, + enum: [ + 'same_city_only', + 'nearby_cities', + 'whole_province', + 'whole_iran', + 'even_abroad', + 'online_is_fine' + ], + required: false, + default: null + }, + children_status: { + type: [{ + type: String, + enum: ['no_children', 'has_children', 'open_to_children', 'not_now', 'prefer_not_to_say'] + }], + required: false, + default: [] + }, + smoking_status: { + type: String, + enum: ['none', 'sometimes', 'yes', 'doesnt_matter', 'prefer_not_to_say'], required: false, default: null }, @@ -292,7 +444,7 @@ const userSchema = new mongoose.Schema({ }, personal_style: { type: String, - enum: ['formal', 'sporty'], + enum: ['casual', 'classic', 'minimal', 'formal', 'street', 'romantic'], required: false, default: null }, @@ -306,18 +458,13 @@ const userSchema = new mongoose.Schema({ }, income_range: { type: String, - enum: ['under_5m', '5m_10m', '10m_20m', 'over_20m'], + enum: [ + 'under_40m', '40m_50m', '50m_60m', '60m_70m', '70m_80m', + '80m_90m', '90m_100m', '100m_150m', '150m_200m', 'over_200m' + ], required: false, default: null }, - moral_traits: { - type: String, - required: false, - trim: true, - minLength: 1, - maxLength: 1024, - default: null - }, personal_details_public: { type: Boolean, required: false, @@ -618,6 +765,30 @@ const userSchema = new mongoose.Schema({ required: false, default: null }, + // بک‌آپ کلید خصوصیِ E2EE رمزنگاری‌شده با رمز/PIN انتخابیِ کاربر (سرور هیچ‌وقت PIN یا + // کلید خام رو نمی‌بینه، فقط این بلاکِ رمزنگاری‌شده رو ذخیره می‌کنه) — برای بازیابیِ + // هویتِ رمزنگاری وقتی هیچ دستگاه دیگه‌ای لینک‌شده در دسترس نیست + e2ee_key_backup: { + type: { + salt: String, + iv: String, + ciphertext: String, + iterations: Number + }, + required: false, + default: null + }, + // یک رشته‌ی تصادفیِ ۳۲بایتیِ per-account که با درخواستِ احرازهویت‌شده به همین + // اکانت، از هر دستگاهی (وب/اندروید) قابل‌دریافته — به‌جای PIN دستی، به‌عنوانِ + // رمزِ رمزنگاریِ e2ee_key_backup استفاده می‌شه تا بازیابیِ هویتِ E2EE روی دستگاهِ + // جدید کاملاً خودکار و بدون دخالتِ کاربر انجام بشه. توجه: این یعنی تضمینِ + // end-to-end دیگه واقعی نیست — هرکسی که به این فیلد (یا سرور) دسترسی پیدا کنه + // می‌تونه بک‌آپ رو باز کنه؛ این تصمیم آگاهانه به نفعِ هیچ‌وقت‌گم‌نشدنِ پیام‌ها گرفته شده + e2ee_auto_backup_secret: { + type: String, + required: false, + default: null + }, pinned_post_ids: { type: [{ type: mongoose.Schema.Types.ObjectId, diff --git a/package-lock.json b/package-lock.json index a342eeb..015d12f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "nodemon": "^3.1.0", "rate-limit-redis": "^4.3.1", "redis": "^6.2.1", + "sharp": "^0.35.4", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" }, @@ -67,6 +68,16 @@ "node": ">=10.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -545,6 +556,506 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2347,6 +2858,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6734,6 +7254,67 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 7feff57..a90c028 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "nodemon": "^3.1.0", "rate-limit-redis": "^4.3.1", "redis": "^6.2.1", + "sharp": "^0.35.4", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" }, diff --git a/routes/application/account/index.js b/routes/application/account/index.js index 5ff585a..2ae1433 100644 --- a/routes/application/account/index.js +++ b/routes/application/account/index.js @@ -21,7 +21,11 @@ const { startDeviceLink, getDeviceLinkSession, approveDeviceLink, - pollDeviceLink + pollDeviceLink, + getOrCreateAutoBackupSecret, + putKeyBackup, + getKeyBackup, + deleteKeyBackup } = require('../../../controllers/application/account/e2eeController') const router = express.Router() @@ -44,5 +48,9 @@ router.post('/e2ee/device-link', auth, blockCheck, startDeviceLink) router.get('/e2ee/device-link/:linkId', auth, blockCheck, getDeviceLinkSession) router.post('/e2ee/device-link/:linkId/approve', auth, blockCheck, approveDeviceLink) router.get('/e2ee/device-link/:linkId/poll', auth, blockCheck, pollDeviceLink) +router.get('/e2ee/auto-backup-secret', auth, blockCheck, getOrCreateAutoBackupSecret) +router.put('/e2ee/key-backup', auth, blockCheck, putKeyBackup) +router.get('/e2ee/key-backup', auth, blockCheck, getKeyBackup) +router.delete('/e2ee/key-backup', auth, blockCheck, deleteKeyBackup) module.exports = router diff --git a/routes/application/analytics/index.js b/routes/application/analytics/index.js new file mode 100644 index 0000000..cce03b7 --- /dev/null +++ b/routes/application/analytics/index.js @@ -0,0 +1,7 @@ +const express = require('express') +const { trackEvents } = require('../../../controllers/application/analytics/analyticsController') + +const router = express.Router() +router.post('/track', trackEvents) + +module.exports = router diff --git a/routes/application/stories/index.js b/routes/application/stories/index.js index fe4a328..38356ff 100644 --- a/routes/application/stories/index.js +++ b/routes/application/stories/index.js @@ -6,6 +6,7 @@ const auth = require('../../../middlewares/auth') const blockCheck = require('../../../middlewares/blockCheck') const isHalfRegister = require('../../../middlewares/isHalfRegister') const { + applyPhotoFilter, createStoryBase64, createStoryMultipart, createStoryFromPost, @@ -43,6 +44,7 @@ const storyUpload = multer({ }).single('story_file') router.get('/feed', getStoriesFeed) +router.post('/apply-filter', auth, blockCheck, isHalfRegister, applyPhotoFilter) router.post('/create-base64', auth, blockCheck, isHalfRegister, createStoryBase64) // عین /posts/create چندبخشی — برای فایل‌های حجیم (مخصوصاً ویدیو) که تبدیل به base64 توی // حافظه‌ی JS موبایل باعث خطای OOM می‌شد؛ فایل مستقیم از دیسک استریم می‌شه diff --git a/routes/application/verify/index.js b/routes/application/verify/index.js index 42cdf1d..06ae33b 100644 --- a/routes/application/verify/index.js +++ b/routes/application/verify/index.js @@ -1,7 +1,7 @@ const express = require('express') const { setProfileImage, updateProfileImage } = require('../../../controllers/application/verify/setProfileImageController') const { setExpertise, updateExpertise } = require('../../../controllers/application/verify/expertiseController') -const { setGender } = require('../../../controllers/application/verify/genderController') +const { setGender, updateGender } = require('../../../controllers/application/verify/genderController') const { setSizes, updateSizes } = require('../../../controllers/application/verify/sizesController') const { setColors, updateColors } = require('../../../controllers/application/verify/colorsController') const { setCooperationType, updateCooperationType } = require('../../../controllers/application/verify/cooperationTypeController') @@ -21,6 +21,7 @@ router.patch('/profile_image', [blockCheck], [auth], updateProfileImage) router.post('/expertise', setExpertise) router.patch('/expertise', [blockCheck], [auth], updateExpertise) router.post('/gender', setGender) +router.patch('/gender', [blockCheck], [auth], updateGender) router.post('/sizes', setSizes) router.patch('/sizes', [blockCheck], [auth], updateSizes) router.post('/services', setServices) diff --git a/routes/index.js b/routes/index.js index 899b352..beecf92 100644 --- a/routes/index.js +++ b/routes/index.js @@ -35,6 +35,7 @@ const accountRouter = require('./application/account') const giftsRouter = require('./application/gifts') const chatRoomsRouter = require('./application/chat-rooms') const suggestionsRouter = require('./application/suggestions') +const analyticsRouter = require('./application/analytics') const projectsPanelRouter = require('./panel/projects') const postsPanelRouter = require('./panel/posts') @@ -58,10 +59,15 @@ const moderationPanelRouter = require('./panel/moderation') const academyContentPanelRouter = require('./panel/academy-content') const licensesPanelRouter = require('./panel/licenses') const notificationsPanelRouter = require('./panel/notifications') +const analyticsPanelRouter = require('./panel/analytics') +const adminUsersPanelRouter = require('./panel/adminUsers') +const auditLogPanelRouter = require('./panel/auditLog') const auth = require('../middlewares/auth') const blockCheck = require('../middlewares/blockCheck') const adminAuth = require('../middlewares/adminAuth') +const requirePermission = require('../middlewares/requirePermission') +const auditLogger = require('../middlewares/auditLogger') module.exports = (app) => { // App app.use('/api/v1/register', registerRouter) @@ -95,6 +101,7 @@ module.exports = (app) => { app.use('/api/v1/financial', [auth], financialRouter) app.use('/api/v1/tickets', [auth], ticketsRouter) app.use('/api/v1/suggestions', [auth], suggestionsRouter) + app.use('/api/v1/analytics', analyticsRouter) app.use('/api/v1/offers', offerRouter) app.use('/api/v1/advertising', advertisingRouter) app.use('/api/v1/settings', settingsRouter) @@ -103,26 +110,32 @@ module.exports = (app) => { app.use('/api/v1/stories', storiesRouter) // Panel + // فقط متدهای نوشتاری (POST/PUT/PATCH/DELETE) رو خودکار توی AuditLog ثبت می‌کنه — + // برای همه‌ی زیرمسیرهای پنل یکجا، بدون نیاز به افزودنش به تک‌تکِ app.use زیر + app.use('/api/v1/panel', auditLogger) app.use('/api/v1/panel/login', loginPanelRouter) - app.use('/api/v1/panel/users', [adminAuth], usersPanelRouter) - app.use('/api/v1/panel/projects', [adminAuth], projectsPanelRouter) - app.use('/api/v1/panel/posts', [adminAuth], postsPanelRouter) - app.use('/api/v1/panel/expertise', [adminAuth], expertisePanelRouter) - app.use('/api/v1/panel/shop-categories', [adminAuth], shopCategoriesPanelRouter) - app.use('/api/v1/panel/marketplace-shops', [adminAuth], marketplaceShopsPanelRouter) - app.use('/api/v1/panel/wallet-requests', [adminAuth], walletRequestsPanelRouter) - app.use('/api/v1/panel/tickets', [adminAuth], ticketsPanelRouter) - app.use('/api/v1/panel/financial', [adminAuth], financialPanelRouter) - app.use('/api/v1/panel/comments', [adminAuth], commentsPanelRouter) - app.use('/api/v1/panel/advertising', [adminAuth], advertisingPanelRouter) - app.use('/api/v1/panel/shops', [adminAuth], shopsPanelRouter) - app.use('/api/v1/panel/settings', [adminAuth], settingsPanelRouter) - app.use('/api/v1/panel/version', [adminAuth], versionPanelRouter) - app.use('/api/v1/panel/stories', [adminAuth], storiesPanelRouter) - app.use('/api/v1/panel/reports', [adminAuth], reportsPanelRouter) - app.use('/api/v1/panel/suggestions', [adminAuth], suggestionsPanelRouter) - app.use('/api/v1/panel/moderation', [adminAuth], moderationPanelRouter) - app.use('/api/v1/panel/academy-content', [adminAuth], academyContentPanelRouter) - app.use('/api/v1/panel/licenses', [adminAuth], licensesPanelRouter) - app.use('/api/v1/panel/notifications', [adminAuth], notificationsPanelRouter) + app.use('/api/v1/panel/users', [adminAuth, requirePermission('users')], usersPanelRouter) + app.use('/api/v1/panel/projects', [adminAuth, requirePermission('projects')], projectsPanelRouter) + app.use('/api/v1/panel/posts', [adminAuth, requirePermission('posts')], postsPanelRouter) + app.use('/api/v1/panel/expertise', [adminAuth, requirePermission('expertise')], expertisePanelRouter) + app.use('/api/v1/panel/shop-categories', [adminAuth, requirePermission('shop-categories')], shopCategoriesPanelRouter) + app.use('/api/v1/panel/marketplace-shops', [adminAuth, requirePermission('marketplace-shops')], marketplaceShopsPanelRouter) + app.use('/api/v1/panel/wallet-requests', [adminAuth, requirePermission('wallet-requests')], walletRequestsPanelRouter) + app.use('/api/v1/panel/tickets', [adminAuth, requirePermission('tickets')], ticketsPanelRouter) + app.use('/api/v1/panel/financial', [adminAuth, requirePermission('financial')], financialPanelRouter) + app.use('/api/v1/panel/comments', [adminAuth, requirePermission('comments')], commentsPanelRouter) + app.use('/api/v1/panel/advertising', [adminAuth, requirePermission('advertising')], advertisingPanelRouter) + app.use('/api/v1/panel/shops', [adminAuth, requirePermission('shops')], shopsPanelRouter) + app.use('/api/v1/panel/settings', [adminAuth, requirePermission('settings')], settingsPanelRouter) + app.use('/api/v1/panel/version', [adminAuth, requirePermission('version')], versionPanelRouter) + app.use('/api/v1/panel/stories', [adminAuth, requirePermission('stories')], storiesPanelRouter) + app.use('/api/v1/panel/reports', [adminAuth, requirePermission('reports')], reportsPanelRouter) + app.use('/api/v1/panel/suggestions', [adminAuth, requirePermission('suggestions')], suggestionsPanelRouter) + app.use('/api/v1/panel/moderation', [adminAuth, requirePermission('moderation')], moderationPanelRouter) + app.use('/api/v1/panel/analytics', [adminAuth, requirePermission('analytics')], analyticsPanelRouter) + app.use('/api/v1/panel/academy-content', [adminAuth, requirePermission('academy-content')], academyContentPanelRouter) + app.use('/api/v1/panel/licenses', [adminAuth, requirePermission('licenses')], licensesPanelRouter) + app.use('/api/v1/panel/notifications', [adminAuth, requirePermission('notifications')], notificationsPanelRouter) + app.use('/api/v1/panel/admin-users', [adminAuth], adminUsersPanelRouter) + app.use('/api/v1/panel/audit-log', [adminAuth], auditLogPanelRouter) } diff --git a/routes/panel/adminUsers/index.js b/routes/panel/adminUsers/index.js new file mode 100644 index 0000000..e2e3d95 --- /dev/null +++ b/routes/panel/adminUsers/index.js @@ -0,0 +1,16 @@ +const express = require('express') +const { + listAdmins, + createAdmin, + updateAdminPermissions, + deleteAdmin +} = require('../../../controllers/panel/adminUsers/adminUsersController') +const requireSuperAdmin = require('../../../middlewares/requireSuperAdmin') + +const router = express.Router() +router.get('/', [requireSuperAdmin], listAdmins) +router.post('/', [requireSuperAdmin], createAdmin) +router.put('/:id/permissions', [requireSuperAdmin], updateAdminPermissions) +router.delete('/:id', [requireSuperAdmin], deleteAdmin) + +module.exports = router diff --git a/routes/panel/analytics/index.js b/routes/panel/analytics/index.js new file mode 100644 index 0000000..10708fd --- /dev/null +++ b/routes/panel/analytics/index.js @@ -0,0 +1,19 @@ +const express = require('express') +const { + getDemographics, + getDeviceStats, + getPageStats, + getClickStats, + getRetentionStats, + getSessionOverview +} = require('../../../controllers/panel/analytics/analyticsPanelController') + +const router = express.Router() +router.get('/demographics', getDemographics) +router.get('/devices', getDeviceStats) +router.get('/pages', getPageStats) +router.get('/clicks', getClickStats) +router.get('/retention', getRetentionStats) +router.get('/sessions', getSessionOverview) + +module.exports = router diff --git a/routes/panel/auditLog/index.js b/routes/panel/auditLog/index.js new file mode 100644 index 0000000..e169911 --- /dev/null +++ b/routes/panel/auditLog/index.js @@ -0,0 +1,9 @@ +const express = require('express') +const { listAuditLog, listAdminsForFilter } = require('../../../controllers/panel/auditLog/auditLogController') +const requireSuperAdmin = require('../../../middlewares/requireSuperAdmin') + +const router = express.Router() +router.get('/', [requireSuperAdmin], listAuditLog) +router.get('/admins', [requireSuperAdmin], listAdminsForFilter) + +module.exports = router diff --git a/routes/panel/moderation/index.js b/routes/panel/moderation/index.js index 9e3e9e1..000eef9 100644 --- a/routes/panel/moderation/index.js +++ b/routes/panel/moderation/index.js @@ -2,8 +2,12 @@ const express = require('express') const { getModerationStats, } = require('../../../controllers/panel/moderation/moderationStatsController') +const { + getOverallStats, +} = require('../../../controllers/panel/moderation/overallStatsController') const router = express.Router() router.get('/stats', getModerationStats) +router.get('/stats/overall', getOverallStats) module.exports = router diff --git a/services/OfferReplyCron.js b/services/OfferReplyCron.js new file mode 100644 index 0000000..d08a315 --- /dev/null +++ b/services/OfferReplyCron.js @@ -0,0 +1,24 @@ +const cron = require('node-cron') +const { refundExpiredOfferCommissions } = require('../utils/offerReplyGate') + +let ioInstance = null + +/** Called once from index.js right after the socket.io server is created — cron jobs have no `req`. */ +function setIo (io) { + ioInstance = io +} + +// هر ۱۵ دقیقه یک‌بار — چون مهلت ۱۲ ساعته است، بازه‌ی کوتاه‌تر از cron ماهانه‌ی +// پروژه‌ها (ProjectCron.js) لازم است تا بازگشتِ وجه با تأخیرِ زیاد انجام نشود +cron.schedule('*/15 * * * *', async () => { + try { + const refundedCount = await refundExpiredOfferCommissions(ioInstance) + if (refundedCount > 0) { + console.log(`[OfferReplyCron] refunded ${refundedCount} expired offer commission(s)`) + } + } catch (error) { + console.error('[OfferReplyCron] error:', error) + } +}) + +module.exports = { setIo } diff --git a/utils/ensureGeoData.js b/utils/ensureGeoData.js index 53a5e98..9594056 100644 --- a/utils/ensureGeoData.js +++ b/utils/ensureGeoData.js @@ -16,21 +16,24 @@ async function ensureGeoData(force = false) { const cities = JSON.parse(fs.readFileSync(path.join(dataDir, 'cities.json'), 'utf8')) const provincesWithFaCount = provinces.filter((p) => p.name_fa).length + const citiesWithFaCount = cities.filter((c) => c.name_fa).length - const [countryCount, provinceCount, cityCount, provinceFaCount] = await Promise.all([ + const [countryCount, provinceCount, cityCount, provinceFaCount, cityFaCount] = await Promise.all([ CountryModel.countDocuments(), ProvinceModel.countDocuments(), CityModel.countDocuments(), - ProvinceModel.countDocuments({ name_fa: { $exists: true, $ne: null } }) + ProvinceModel.countDocuments({ name_fa: { $exists: true, $ne: null } }), + CityModel.countDocuments({ name_fa: { $exists: true, $ne: null } }) ]) const inSync = countryCount === countries.length && provinceCount === provinces.length && cityCount === cities.length && - provinceFaCount === provincesWithFaCount + provinceFaCount === provincesWithFaCount && + cityFaCount === citiesWithFaCount if (!force && inSync) return - console.log(`[boot] Geo reference data out of sync (countries: ${countryCount}/${countries.length}, provinces: ${provinceCount}/${provinces.length}, cities: ${cityCount}/${cities.length}) — reseeding...`) + console.log(`[boot] Geo reference data out of sync (countries: ${countryCount}/${countries.length}, provinces: ${provinceCount}/${provinces.length}, cities: ${cityCount}/${cities.length}, city name_fa: ${cityFaCount}/${citiesWithFaCount}) — reseeding...`) await CountryModel.deleteMany({}) await ProvinceModel.deleteMany({}) diff --git a/utils/offerReplyGate.js b/utils/offerReplyGate.js index 91827b6..9e28bb7 100644 --- a/utils/offerReplyGate.js +++ b/utils/offerReplyGate.js @@ -1,13 +1,14 @@ const OfferModel = require('../models/OfferModel') const WalletTransactionModel = require('../models/WalletTransactionModel') +const { notifyUser } = require('./notifyUser') -const OFFER_REPLY_WINDOW_MS = 24 * 60 * 60 * 1000 +const OFFER_REPLY_WINDOW_MS = 12 * 60 * 60 * 1000 /** * Called whenever a chat message is sent. If there's a pending paid Offer * between these two users, tracks the requester's first direct message and * the receiver's reply — the receiver's 30% commission is only credited if - * they reply within 24 hours of that first message. + * they reply within 12 hours of that first message. */ async function trackOfferMessageForCommission (io, senderId, receiverId) { try { @@ -33,7 +34,7 @@ async function trackOfferMessageForCommission (io, senderId, receiverId) { return } - // پیام از طرف گیرنده — کسی که باید ظرف ۲۴ ساعت پاسخ بدهد + // پیام از طرف گیرنده — کسی که باید ظرف ۱۲ ساعت پاسخ بدهد if (!offer.firstMessageAt || offer.repliedAt) return if (new Date() > offer.replyDeadline) return @@ -65,4 +66,54 @@ async function trackOfferMessageForCommission (io, senderId, receiverId) { } } -module.exports = { trackOfferMessageForCommission, OFFER_REPLY_WINDOW_MS } +/** + * Sweep for paid Offers whose 12-hour reply deadline has passed without the + * receiver replying — refunds the pending 30% commission to the sender's + * wallet instead (both web and Android go through the same payment callback, + * so a single server-side sweep covers both). Meant to be called + * periodically by a cron job (see services/OfferReplyCron.js). + */ +async function refundExpiredOfferCommissions (io) { + const now = new Date() + const expiredOffers = await OfferModel.find({ + commissionCredited: false, + commissionRefunded: false, + pendingCommissionAmount: { $gt: 0 }, + firstMessageAt: { $ne: null }, + repliedAt: null, + replyDeadline: { $lte: now } + }) + + for (const offer of expiredOffers) { + try { + offer.commissionRefunded = true + await offer.save() + + await WalletTransactionModel.create({ + user: offer.sender, + bucket: 'gift', + type: 'credit', + amount: offer.pendingCommissionAmount, + referenceType: 'payment', + referenceId: offer._id, + description: 'بازگشت وجه درخواست همکاری (طرف مقابل ظرف ۱۲ ساعت مقرر پاسخ نداد)' + }) + + await notifyUser(io, offer.sender, { + type: 'offer_commission_refunded', + title: 'بازگشت وجه به کیف پول', + description: 'طرف مقابل ظرف مهلت ۱۲ ساعته به درخواست همکاری شما پاسخ نداد؛ مبلغ به کیف پول شما بازگشت.' + }) + } catch (error) { + console.error('refundExpiredOfferCommissions error for offer', offer._id, error) + } + } + + return expiredOffers.length +} + +module.exports = { + trackOfferMessageForCommission, + refundExpiredOfferCommissions, + OFFER_REPLY_WINDOW_MS +} diff --git a/utils/panelPermissions.js b/utils/panelPermissions.js new file mode 100644 index 0000000..32042b9 --- /dev/null +++ b/utils/panelPermissions.js @@ -0,0 +1,33 @@ +// فهرست همه‌ی بخش‌های پنل که می‌شه دسترسیِ جداگانه بهشون داد — هر کلید دقیقاً معادلِ +// یه مسیر پنل (routes/index.js: app.use('/api/v1/panel/', ...)). وقتی بخش جدیدی +// به پنل اضافه شد، اینجا هم باید اضافه بشه وگرنه staff هیچ‌وقت بهش دسترسی نمی‌گیره +const PANEL_PERMISSIONS = [ + { key: 'users', label: 'کاربران' }, + { key: 'projects', label: 'پروژه‌ها' }, + { key: 'posts', label: 'پست‌ها' }, + { key: 'expertise', label: 'تخصص‌ها' }, + { key: 'shop-categories', label: 'دسته‌بندی فروشگاه' }, + { key: 'marketplace-shops', label: 'فروشگاه‌های مارکت‌پلیس' }, + { key: 'wallet-requests', label: 'درخواست‌های کیف پول' }, + { key: 'tickets', label: 'تیکت‌ها' }, + { key: 'financial', label: 'مالی' }, + { key: 'comments', label: 'نظرات' }, + { key: 'advertising', label: 'بیلبوردها' }, + { key: 'shops', label: 'فروشگاه‌های بیلبورد' }, + { key: 'settings', label: 'تنظیمات (مالیات/قیمت/CDN/SSL/ترافیک)' }, + { key: 'version', label: 'نسخه اپلیکیشن' }, + { key: 'stories', label: 'استوری‌ها' }, + { key: 'reports', label: 'گزارش‌های محتوا' }, + { key: 'suggestions', label: 'پیشنهادها' }, + { key: 'moderation', label: 'داشبورد بازبینی' }, + { key: 'analytics', label: 'آنالیتیکس' }, + { key: 'academy-content', label: 'محتوای آموزشگاه' }, + { key: 'licenses', label: 'مجوزها' }, + { key: 'notifications', label: 'اعلان‌ها' }, + { key: 'user-management', label: 'مدیریت کاربران پنل' }, + { key: 'system-history', label: 'تاریخچه سیستم' } +] + +const PANEL_PERMISSION_KEYS = new Set(PANEL_PERMISSIONS.map((p) => p.key)) + +module.exports = { PANEL_PERMISSIONS, PANEL_PERMISSION_KEYS } diff --git a/utils/partnerMatch.js b/utils/partnerMatch.js index ebc37b5..ee36fcd 100644 --- a/utils/partnerMatch.js +++ b/utils/partnerMatch.js @@ -55,6 +55,43 @@ function matchesCriteria (user, criteria = {}) { if (!user.job || !user.job.toLowerCase().includes(criteria.job.toLowerCase())) return false } + // Multi-select dating-profile fields: match if the candidate's stored + // array overlaps ANY of the alert's selected values — mirrors the $in + // semantics of criteriaToMongoFilter in partnerSearchController.js. + if (criteria.relationship_goal?.length) { + const userValues = user.relationship_goal || [] + if (!criteria.relationship_goal.some((v) => userValues.includes(v))) return false + } + if (criteria.personality_traits?.length) { + const userValues = user.personality_traits || [] + if (!criteria.personality_traits.some((v) => userValues.includes(v))) return false + } + if (criteria.lifestyle?.length) { + const userValues = user.lifestyle || [] + if (!criteria.lifestyle.some((v) => userValues.includes(v))) return false + } + if (criteria.interests?.length) { + const userValues = user.interests || [] + if (!criteria.interests.some((v) => userValues.includes(v))) return false + } + if (criteria.values?.length) { + const userValues = user.values || [] + if (!criteria.values.some((v) => userValues.includes(v))) return false + } + + // Single-select dating-profile fields: exact match. + if (criteria.seeking_gender && user.seeking_gender !== criteria.seeking_gender) return false + if (criteria.relationship_view && user.relationship_view !== criteria.relationship_view) return false + if (criteria.geo_preference && user.geo_preference !== criteria.geo_preference) return false + if (criteria.smoking_status && user.smoking_status !== criteria.smoking_status) return false + + // children_status is multi-select (a user may pick more than one status) — + // same overlap semantics as the multi-select fields above. + if (criteria.children_status?.length) { + const userValues = user.children_status || [] + if (!criteria.children_status.some((v) => userValues.includes(v))) return false + } + if (criteria.country_id && user.country?.id !== criteria.country_id) return false if (criteria.province_id && user.province?.id !== criteria.province_id) return false if (criteria.city_id && user.city?.id !== criteria.city_id) return false @@ -67,8 +104,11 @@ function matchesCriteria (user, criteria = {}) { const MATCH_RELEVANT_FIELDS = [ 'gender', 'height', 'weight', 'size', 'birthday', 'hair_color', 'eye_color', 'marital_status', 'residence_type', 'residence_area', 'vehicle_type', - 'vehicle_details', 'personal_style', 'job', 'income_range', 'moral_traits', - 'country', 'province', 'city', 'is_Register' + 'vehicle_details', 'personal_style', 'job', 'income_range', + 'country', 'province', 'city', 'is_Register', + 'relationship_goal', 'seeking_gender', 'desired_age_range', 'personality_traits', + 'lifestyle', 'interests', 'values', 'relationship_view', 'geo_preference', + 'children_status', 'smoking_status' ] function shouldCheck (user) { diff --git a/utils/reelsTabFeed.js b/utils/reelsTabFeed.js index ae15fb0..6b576ef 100644 --- a/utils/reelsTabFeed.js +++ b/utils/reelsTabFeed.js @@ -126,6 +126,11 @@ async function getSuggestedFollowUsers(userId, limit = 15) { previewPost: { _id: previewPost._id, type: previewPost.type, + // مدل واقعی مدیا رو با post_images/post_video برمی‌گردونه، نه همیشه files — + // بدون این دو تا، فرانت‌اند (RN/وب) نمی‌تونست فایلِ پیش‌نمایش رو پیدا کنه و + // کارت کاملاً خالی می‌موند + post_images: previewPost.post_images, + post_video: previewPost.post_video, files: previewPost.files, caption: previewPost.caption, }, diff --git a/utils/resolveLinkedEntity.js b/utils/resolveLinkedEntity.js index 1f9010e..9af87f5 100644 --- a/utils/resolveLinkedEntity.js +++ b/utils/resolveLinkedEntity.js @@ -5,7 +5,7 @@ const AdvertisingModel = require('../models/AdvertisingModel'); const ShopModel = require('../models/ShopModel'); const ShopProductListingModel = require('../models/ShopProductListingModel'); const BookingConfigModel = require('../models/BookingConfigModel'); -const { PUBLIC_COURSE_STATUSES, PUBLIC_BILLBOARD_STATUSES } = require('./moderationStatus'); +const { PUBLIC_COURSE_STATUSES, PUBLIC_BILLBOARD_STATUSES, PUBLIC_PROJECT_STATUSES } = require('./moderationStatus'); // اعتبارسنجی مشترک «آیتم قابل‌لینک‌شدن» — هم پست (linked_*_id) هم استوری (لینک‌اورلی) از // همین یه تابع استفاده می‌کنن تا شرط مالکیت+وضعیت هرکدوم دوبار نوشته نشه. خروجی: شناسه‌ی @@ -30,10 +30,13 @@ async function resolveLinkedEntity(kind, id, userId) { } case 'project': { const project = await ProjectModel.findById(id).select('_id creator_id status').lean(); + // عین pkg/billboard — هر وضعیتِ واقعی/متعهدشده و غیرِ رد/لغوشده رو می‌پذیریم، نه فقط + // done/accepted؛ وگرنه تقریباً هیچ پروژه‌ی واقعی‌ای (که بیشترِ عمرش pending/ongoing/ + // temp_accept هست، نه دقیقاً done/accepted) قابل‌لینک‌شدن نبود if ( project && String(project.creator_id) === String(userId) && - ['done', 'accepted'].includes(project.status) + PUBLIC_PROJECT_STATUSES.includes(project.status) ) { return String(project._id); } @@ -53,14 +56,20 @@ async function resolveLinkedEntity(kind, id, userId) { } case 'shop': { const shop = await ShopModel.findById(id).select('_id owner status').lean(); - if (shop && String(shop.owner) === String(userId) && shop.status === 'active') { + // draft/rejected هنوز واقعاً وجود عمومی ندارن، ولی pending_review (ثبت‌شده، منتظرِ + // تایید) هم مثل بقیه‌ی موارد بالا باید قابل‌لینک باشه — فقط active بیش‌ازحد سخت‌گیرانه بود + if ( + shop && + String(shop.owner) === String(userId) && + ['active', 'pending_review'].includes(shop.status) + ) { return String(shop._id); } return null; } case 'shopproduct': { const listing = await ShopProductListingModel.findById(id).select('_id shop status').lean(); - if (!listing || listing.status !== 'active') return null; + if (!listing || !['active', 'pending_shop_approval'].includes(listing.status)) return null; const shop = await ShopModel.findById(listing.shop).select('_id owner').lean(); if (shop && String(shop.owner) === String(userId)) { return String(listing._id); diff --git a/utils/storyPhotoFilters.js b/utils/storyPhotoFilters.js new file mode 100644 index 0000000..45e472d --- /dev/null +++ b/utils/storyPhotoFilters.js @@ -0,0 +1,24 @@ +const sharp = require('sharp') + +// فیلترهای رنگ/نورِ سبک اینستاگرام — با sharp (رایگان، بدون لایسنس/کلید API، فقط +// پردازش تصویر روی سرور). هر پیش‌تنظیم ترکیبی از modulate (اشباع/روشنایی/چرخش‌رنگ)، +// tint (رنگ‌آمیزیِ کلی برای حس گرم/سرد/قدیمی) و linear (کنتراست) است. +const STORY_PHOTO_FILTERS = { + normal: (img) => img, + warm: (img) => img.modulate({ brightness: 1.05, saturation: 1.15 }).tint({ r: 255, g: 230, b: 200 }), + cool: (img) => img.modulate({ brightness: 1.0, saturation: 1.1 }).tint({ r: 210, g: 230, b: 255 }), + bw: (img) => img.grayscale().modulate({ brightness: 1.05 }), + vivid: (img) => img.modulate({ saturation: 1.6, brightness: 1.08 }).sharpen(), + fade: (img) => img.modulate({ saturation: 0.65, brightness: 1.12 }).linear(0.85, 25), + vintage: (img) => img.modulate({ saturation: 0.75, brightness: 1.02 }).tint({ r: 255, g: 235, b: 205 }) +} + +const STORY_PHOTO_FILTER_NAMES = Object.keys(STORY_PHOTO_FILTERS) + +/** buffer ورودی -> بافرِ jpeg خروجی با فیلترِ اعمال‌شده. filterName نامعتبر => همون تصویر اصلی. */ +async function applyStoryPhotoFilter (buffer, filterName) { + const apply = STORY_PHOTO_FILTERS[filterName] || STORY_PHOTO_FILTERS.normal + return apply(sharp(buffer)).jpeg({ quality: 90 }).toBuffer() +} + +module.exports = { STORY_PHOTO_FILTERS, STORY_PHOTO_FILTER_NAMES, applyStoryPhotoFilter } diff --git a/utils/usernameValidation.js b/utils/usernameValidation.js index 9f3d17f..ff85b33 100644 --- a/utils/usernameValidation.js +++ b/utils/usernameValidation.js @@ -1,5 +1,5 @@ /** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */ -const USERNAME_MIN_LENGTH = 8 +const USERNAME_MIN_LENGTH = 6 const USERNAME_MAX_LENGTH = 30 /** first char letter/digit, then up to (max-1) allowed chars */