1+ import { toEnglishDigits } from "@/utils/format" ;
2+ import { ofetch } from "ofetch" ;
3+
4+ const CITY = '1' ; // Tehran city ID
5+
6+ const httpClient = ofetch . create ( {
7+ baseURL : 'https://api.divar.ir' ,
8+ retryDelay : 30000 ,
9+ retry : 5 ,
10+ retryStatusCodes : [ 429 , 500 ] ,
11+ responseType : 'json' ,
12+ headers : {
13+ 'Content-Type' : 'application/json' ,
14+ } ,
15+ } )
16+
17+ export type House = {
18+ location : { lat : number , lng : number } | null ,
19+ size : number | null ,
20+ beds : number | null ,
21+ totalPrice : number | null ,
22+ unitPrice : number | null ,
23+ elevator : boolean | null ,
24+ storage : boolean | null ,
25+ parking : boolean | null ,
26+ balcony : boolean | null ,
27+ yearBuilt : number | null ,
28+ }
29+ export const getHouse = ( id : string ) : Promise < House > => {
30+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31+ return httpClient ( `/v8/posts-v2/web/${ id } ` ) . then ( ( resp : any ) => {
32+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33+ let fields : Record < string , any > = { } ;
34+
35+ for ( const section of ( resp ?. sections ?? [ ] ) ) {
36+ for ( const widget of ( section ?. widgets ?? [ ] ) ) {
37+ const widgetType = widget ?. widget_type ;
38+ if ( widgetType === 'GROUP_INFO_ROW' || widgetType === 'GROUP_FEATURE_ROW' || widgetType === 'UNEXPANDABLE_ROW' ) {
39+ for ( const item of ( widget ?. data ?. items ?? [ widget ?. data ] ) ) {
40+ if ( item ?. title ) {
41+ fields = {
42+ ...fields ,
43+ [ item . title ] : item . available ?? toEnglishDigits ( item . value ) . replace ( / [ ^ \d \s ] / g, '' ) ,
44+ } ;
45+ }
46+ }
47+ }
48+ if ( widgetType === 'MAP_ROW' && ( widget ?. data ?. location ?. fuzzy_data ?. point || widget ?. data ?. location ?. exact_data ?. point ) ) {
49+ fields = {
50+ ...fields ,
51+ location : widget . data . location . exact_data ?. point ?? widget . data . location . fuzzy_data ?. point ,
52+ } ;
53+ }
54+ }
55+ }
56+
57+ return {
58+ location : typeof fields . location ?. latitude === 'number' && typeof fields . location ?. longitude === 'number' ? { lat : fields . location ?. latitude , lng : fields . location . longitude } : null ,
59+ size : typeof fields [ 'متراژ' ] === 'string' && fields [ 'متراژ' ] . trim ( ) !== '' ? + fields [ 'متراژ' ] : null ,
60+ beds : typeof fields [ 'اتاق' ] === 'string' && fields [ 'اتاق' ] . trim ( ) !== '' ? + fields [ 'اتاق' ] : null ,
61+ // floor: typeof fields['طبقه'] === 'string' && fields['طبقه'].trim() !== '' ? fields['طبقه'].split(' ').filter(x=>!!x).map(x => +x) as [number, number] | [number] : null,
62+ totalPrice : typeof fields [ 'قیمت کل' ] === 'string' && fields [ 'قیمت کل' ] . trim ( ) !== '' ? + fields [ 'قیمت کل' ] : null ,
63+ unitPrice : typeof fields [ 'قیمت هر متر' ] === 'string' && fields [ 'قیمت هر متر' ] . trim ( ) !== '' ? + fields [ 'قیمت هر متر' ] : null ,
64+ elevator : 'آسانسور ندارد' in fields || fields [ 'آسانسور' ] === false ? false : fields [ 'آسانسور' ] === true ? true : null ,
65+ storage : 'انباری ندارد' in fields || fields [ 'انباری' ] === false ? false : fields [ 'انباری' ] === true ? true : null ,
66+ parking : 'پارکینگ ندارد' in fields || fields [ 'پارکینگ' ] === false ? false : fields [ 'پارکینگ' ] === true ? true : null ,
67+ balcony : 'بالکن ندارد' in fields || fields [ 'بالکن' ] === false ? false : fields [ 'بالکن' ] === true ? true : null ,
68+ yearBuilt : typeof fields [ 'ساخت' ] === 'string' && fields [ 'ساخت' ] !== '' ? + fields [ 'ساخت' ] : null ,
69+ } ;
70+ } ) ;
71+ }
72+
73+
74+ export type District = {
75+ title : string ,
76+ value : string ,
77+ hint : string ,
78+ keywords : string [ ] ,
79+ }
80+
81+ export const getDistricts = ( ) : Promise < District [ ] > => {
82+ return httpClient (
83+ "/v8/postlist/w/filters" ,
84+ {
85+ method : 'POST' ,
86+ body : {
87+ city_ids : [ CITY ] ,
88+ source_view : "FILTER" ,
89+ data : { } ,
90+ } ,
91+ }
92+ )
93+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94+ . then ( ( resp : any ) => {
95+ let returnValue : District [ ] = [ ] ;
96+ for ( const widget of ( resp ?. page ?. widget_list ?? [ ] ) ) {
97+ for ( const subWidget of ( widget ?. data ?. widget_list ?? [ ] ) ) {
98+ for ( const district of ( subWidget ?. data ?. neighborhoods ?. options ?? [ ] ) ) {
99+ returnValue = [
100+ ...returnValue ,
101+ {
102+ title : district . title as string ,
103+ value : district . value as string ,
104+ hint : district . hint as string ,
105+ keywords : district . search_keywords . split ( '،' ) . map ( ( x : string ) => x . trim ( ) )
106+ } ,
107+ ] ;
108+ }
109+ }
110+ }
111+ return returnValue ;
112+ } ) ;
113+ }
114+
115+ export type SearchHousesFilters = {
116+ district ?: string [ ] ;
117+ elevator ?: boolean ;
118+ parking ?: boolean ;
119+ balcony ?: boolean ;
120+ size ?: [ number , number ] ;
121+ price ?: [ number , number ] ;
122+ }
123+
124+ export const getHousesIds = ( filters : SearchHousesFilters ) : Promise < string [ ] > => {
125+ return httpClient ( "/v8/postlist/w/search" , {
126+ method : 'POST' ,
127+ body : {
128+ city_ids : [ CITY ] ,
129+ source_view : 'FILTER' ,
130+ disable_recommendation : false ,
131+ search_data : {
132+ form_data : {
133+ data : {
134+ // bbox: {
135+ // repeated_float: {
136+ // value: [
137+ // { value: 51.4265289 },
138+ // { value: 35.7938423 },
139+ // { value: 51.4346771 },
140+ // { value: 35.8068733 },
141+ // ],
142+ // },
143+ // },
144+ deed_type : { repeated_string : { value : [ 'single_page' ] } } ,
145+ category : { str : { value : "apartment-sell" } } ,
146+ ...( typeof filters . district !== 'undefined' && {
147+ districts : { repeated_string : { value : filters . district } } ,
148+ } ) ,
149+ ...( typeof filters . elevator !== 'undefined' && {
150+ elevator : { boolean : { value : filters . elevator } } ,
151+ } ) ,
152+ ...( typeof filters . parking !== 'undefined' && {
153+ parking : { boolean : { value : filters . parking } } ,
154+ } ) ,
155+ ...( typeof filters . balcony !== 'undefined' && {
156+ balcony : { boolean : { value : filters . balcony } } ,
157+ } ) ,
158+ ...( typeof filters . size !== 'undefined' && {
159+ size : { number_range : { minimum : filters . size [ 0 ] , maximum : filters . size [ 1 ] } } ,
160+ } ) ,
161+ ...( typeof filters . price !== 'undefined' && {
162+ price : { number_range : { minimum : filters . price [ 0 ] , maximum : filters . price [ 1 ] } } ,
163+ } ) ,
164+ } ,
165+ } ,
166+ } ,
167+ } ,
168+ } )
169+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170+ . then ( ( resp : any ) => {
171+ let returnValue : string [ ] = [ ] ;
172+ for ( const widget of ( resp ?. list_widgets ?? [ ] ) ) {
173+ if ( widget ?. widget_type !== 'POST_ROW' || ! widget ?. data ?. middle_description_text ?. includes ( 'تومان' ) || ! widget ?. data ?. token ) continue ;
174+ returnValue = [ ...returnValue , widget . data . token ] ;
175+ }
176+ return returnValue ;
177+ } ) ;
178+ }
179+
180+
181+ export const getHouses = async ( filters : SearchHousesFilters , progressFn ?: ( value : number , lastValue : House [ ] ) => void ) : Promise < House [ ] > => {
182+ let returnValue : House [ ] = [ ] ;
183+ let currentProgress = 0 ;
184+ const progress = ( value : number ) => {
185+ currentProgress = value ;
186+ progressFn ?.( currentProgress , returnValue ) ;
187+ }
188+ progress ( 0 ) ;
189+ const districts = await getDistricts ( ) ;
190+
191+ let passedDistricts = 0 ;
192+ for ( const district of districts ) {
193+ progress ( passedDistricts / districts . length ) ;
194+ const districtHouseIds = await getHousesIds ( {
195+ ...filters ,
196+ district : [ district . value ] ,
197+ } ) ;
198+
199+
200+ for ( const houseId of districtHouseIds ) {
201+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
202+ returnValue = [ ...returnValue , await getHouse ( houseId ) ] ;
203+ progress ( ( passedDistricts + ( districtHouseIds . indexOf ( houseId ) / districtHouseIds . length ) ) / districts . length ) ;
204+ }
205+
206+ passedDistricts += 1 ;
207+ }
208+ progress ( 1 ) ;
209+ return returnValue ;
210+ }
0 commit comments