Skip to content

Commit 72e7de7

Browse files
committed
⚒️wip: add delete item feat via CSRF-Token extractor
1 parent 53d7fc0 commit 72e7de7

File tree

5 files changed

+121
-135
lines changed

5 files changed

+121
-135
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ __pycache__/
2929
dist/
3030
alembic/
3131
alembic.ini
32-
accounting/
32+
accounting/
33+
tests_py/

backend/config/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
DEBUG_MODE = True
66
ALLOW_ORIGINS = ["http://localhost:5173"]
7-
7+
ALLOW_METHODS = ["GET", "POST", "DELETE", "PATCH"]
88

99
# DB might change, I just use sqlite for now
1010
sqlite_file_name = "database.db"

backend/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi.middleware.cors import CORSMiddleware
22
from fastapi import FastAPI
3-
from config.settings import ALLOW_ORIGINS, DEBUG_MODE
3+
from config.settings import ALLOW_ORIGINS, DEBUG_MODE, ALLOW_METHODS
44
from routers import ads_management, auth, follow_mass, accounting
55
from config.models import lifespan
66

@@ -18,6 +18,6 @@
1818
app.add_middleware(
1919
CORSMiddleware,
2020
allow_origins=ALLOW_ORIGINS,
21-
allow_methods=["GET", "POST", "DELETE"],
21+
allow_methods=ALLOW_METHODS,
2222
allow_credentials=True,
2323
)

backend/routers/ads_management.py

Lines changed: 115 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
"""
2-
TODO:
3-
- add a input front to select the number of n last ads to refresh
4-
- finish the refresh ads function
2+
Notes:
3+
- Vinted uses SSR and item data is hidden in html, its a pain to extract and I did not manage
4+
They are building an API for pro users: https://pro-docs.svc.vinted.com/#vinted-pro-integrations-documentation-items-api
55
"""
66

77
import json
88
import requests
99
import re
10-
from bs4 import BeautifulSoup
1110
from time import sleep
12-
from typing import List
1311
from fastapi import APIRouter, Depends
14-
from pydantic import BaseModel
1512
from utils import execute_request, get_vinted_headers
1613
from constants import API_URL
1714
from sqlmodel import Session, select
@@ -22,74 +19,18 @@
2219
)
2320

2421

25-
class PhotoThumbnail(BaseModel):
26-
type: str
27-
url: str
28-
29-
30-
class Photo(BaseModel):
31-
thumbnails: List[PhotoThumbnail]
32-
33-
34-
class Price(BaseModel):
35-
currency_code: str
36-
amount: float
37-
38-
39-
def extract_photos_urls(photos: List[Photo]) -> List[str]:
40-
"""Extracts photo URLs from photos data"""
41-
urls = []
42-
for photo in photos:
43-
for thumbnail in photo.thumbnails:
44-
if thumbnail.type == "thumb364x428":
45-
urls.append(thumbnail.url)
46-
return urls
47-
48-
49-
def prepare_item_info(item: dict) -> dict:
50-
pass
51-
52-
53-
async def upload_photos(urls: List[str], temp_uuid: str, headers: dict) -> List[int]:
54-
"""Uploads photos and returns list of photo IDs"""
55-
photo_ids = []
56-
57-
for i, url in enumerate(urls):
58-
response = requests.get(url)
59-
if response.status_code != 200:
60-
continue
61-
62-
files = [("photo[file]", (f"{i}.jpg", response.content, "image/jpeg"))]
63-
payload = {"photo[type]": "item", "photo[temp_uuid]": temp_uuid}
64-
65-
response = requests.post(
66-
"https://www.vinted.fr/api/v2/photos",
67-
headers=headers,
68-
data=payload,
69-
files=files,
70-
)
71-
72-
if response.status_code == 200:
73-
photo_ids.append(response.json()["id"])
74-
75-
return photo_ids
76-
77-
7822
@router.get("/refresh-ads")
7923
async def refresh_ads(
8024
headers: dict = Depends(get_vinted_headers), session: Session = Depends(get_session)
8125
):
8226
"""Refreshes ads available for the authenticated profile"""
83-
same_row = 0
8427
page = 1
8528

8629
url = f"{API_URL}wardrobe/4977264403/items?page={page}&per_page=20&order=revelance"
8730
response = execute_request("GET", url, headers)
8831

8932
total_pages = response.json()["pagination"]["total_pages"]
9033

91-
print("Il y a", total_pages, "pages")
92-
9334
while page <= total_pages:
9435
user = session.exec(select(User).where(User.id == 1)).first()
9536
url = f"{API_URL}wardrobe/{user.userId}/items?page={page}&per_page=20&order=revelance"
@@ -114,65 +55,32 @@ async def refresh_ads(
11455

11556
response = execute_request("GET", item_url, headers)
11657

117-
result = re.search(
118-
r'(\{"itemDto":.*?"electronicsVerification":.*?\}\})\]',
119-
response.text.replace('\\"', '"').replace("\\\\n", "\\n"),
120-
)
121-
if result:
122-
item_dto_str = result.group(1)
123-
124-
try:
125-
item_data = json.loads(item_dto_str)
126-
except json.JSONDecodeError as e:
127-
print(f"Error parsing JSON: {e}")
58+
item_data = extract_item_json(response.text)
59+
60+
if item_data:
61+
return {"message": "Not implemented"}
62+
63+
"""
64+
Roadmap to repost an item:
65+
1) Download current photos
66+
2) Upload new photos
67+
3) Delete item
68+
4) Upload item
69+
"""
70+
71+
# 3) Delete item before reposting it
72+
if delete_item(item["path"], item["id"], headers):
73+
print(f"Item: {item_data['title']} deleted successfully")
74+
else:
75+
print(f"Item: {item_data['title']} not deleted, skipping")
76+
continue
77+
78+
# 4) Upload item
79+
80+
print(f"Item: {item_data['title']} reposted successfully")
12881
else:
129-
print("No item data found in the response")
130-
131-
return {
132-
"message": "Item data fetched successfully",
133-
}
134-
135-
# Prepare item data
136-
# item_info = prepare_item_info(item)
137-
# photo_urls = extract_photos_urls(item["photos"])
138-
139-
# # Handle rate limiting
140-
# if same_row == 15:
141-
# sleep(30)
142-
# same_row = 0
143-
144-
# # Get temp UUID for upload
145-
# response = requests.get("https://www.vinted.fr/items/new", headers=headers)
146-
# temp_uuid = (
147-
# re.search(
148-
# r'<div id="ItemUpload-react-component-\s*(.*?)\s*"',
149-
# response.text,
150-
# re.DOTALL,
151-
# )
152-
# .group(1)
153-
# .strip()
154-
# )
155-
156-
# # Upload photos
157-
# photo_ids = await upload_photos(photo_urls, temp_uuid, headers)
158-
159-
# # Prepare final item data
160-
# item_info["temp_uuid"] = temp_uuid
161-
# item_info["assigned_photos"] = [
162-
# {"id": pid, "orientation": 0} for pid in photo_ids
163-
# ]
164-
165-
# Upload item
166-
# response = requests.post(
167-
# "https://www.vinted.fr/api/v2/items",
168-
# headers=headers,
169-
# json={"item": item_info, "feedback_id": None},
170-
# )
171-
172-
# if response.status_code == 200:
173-
# # Delete old item
174-
# requests.post(f"{API_URL}items/{item['id']}/delete", headers=headers)
175-
# same_row += 1
82+
print("No item data found in the response, skipping")
83+
continue
17684

17785
page += 1
17886

@@ -183,7 +91,6 @@ async def refresh_ads(
18391
async def delete_sold_items(
18492
headers: dict = Depends(get_vinted_headers), session: Session = Depends(get_session)
18593
):
186-
"""Deletes all sold items"""
18794
page = 1
18895

18996
while True:
@@ -211,10 +118,7 @@ async def delete_sold_items(
211118
sleep(30)
212119
nb_items_deleted = 0
213120

214-
response = execute_request(
215-
"delete", f"{API_URL}items/{item['id']}/delete", headers
216-
)
217-
if response.status_code == 200:
121+
if delete_item(item["path"], item["id"], headers):
218122
nb_items_deleted += 1
219123

220124
page += 1
@@ -226,7 +130,6 @@ async def delete_sold_items(
226130
async def delete_all_ads(
227131
headers: dict = Depends(get_vinted_headers), session: Session = Depends(get_session)
228132
):
229-
"""Deletes all ads"""
230133
page = 1
231134

232135
while True:
@@ -250,12 +153,94 @@ async def delete_all_ads(
250153
sleep(30)
251154
nb_items_deleted = 0
252155

253-
response = execute_request(
254-
"delete", f"{API_URL}items/{item['id']}/delete", headers
255-
)
256-
if response.status_code == 200:
156+
if delete_item(item["path"], item["id"], headers):
257157
nb_items_deleted += 1
258158

259159
page += 1
260160

261161
return {"message": "All ads deleted"}
162+
163+
164+
def delete_item(path: str, item_id: int, headers: dict) -> bool:
165+
item_url = f"https://www.vinted.fr{path}"
166+
167+
csrf_token = get_csrf_token(item_url, headers)
168+
if csrf_token:
169+
headers["X-CSRF-Token"] = csrf_token
170+
response = execute_request(
171+
"DELETE", f"{API_URL}items/{item_id}/delete", headers
172+
)
173+
return response.status_code == 200
174+
else:
175+
return False
176+
177+
178+
def get_csrf_token(item_url: str, headers: dict) -> str | None:
179+
response = requests.get(item_url, headers=headers)
180+
181+
if response.status_code == 200:
182+
html_content = response.text
183+
pattern = r'\\"CSRF_TOKEN\\":\\"(.*?)\\"'
184+
185+
matches = re.findall(pattern, html_content)
186+
if matches:
187+
return matches[0]
188+
return None
189+
else:
190+
return None
191+
192+
193+
def extract_item_json(html_content: str):
194+
"""Vinted loves to hide its data in html, this is how to extract with chatGPT"""
195+
# Find all script tags with the pattern
196+
script_matches = re.findall(
197+
r'self\.__next_f\.push\(\[1,"(.*?)"\]\)</script>', html_content, re.DOTALL
198+
)
199+
200+
for json_str in script_matches:
201+
if json_str.startswith("16:"):
202+
json_str = json_str[3:]
203+
204+
json_str = json_str.replace('\\"', '"').replace("\\\\n", "\\n")
205+
206+
# Check if this script contains item_closing_action
207+
if "item_closing_action" in json_str:
208+
# Find the first complete JSON object
209+
brace_count = 0
210+
start = json_str.find("{")
211+
if start != -1:
212+
for i in range(start, len(json_str)):
213+
if json_str[i] == "{":
214+
brace_count += 1
215+
elif json_str[i] == "}":
216+
brace_count -= 1
217+
if brace_count == 0:
218+
json_str = json_str[start : i + 1]
219+
break
220+
221+
try:
222+
data = json.loads(json_str)
223+
224+
# Find item in the structure
225+
def find_item(obj):
226+
if isinstance(obj, dict) and "item" in obj:
227+
return obj["item"]
228+
elif isinstance(obj, dict):
229+
for v in obj.values():
230+
result = find_item(v)
231+
if result:
232+
return result
233+
elif isinstance(obj, list):
234+
for v in obj:
235+
result = find_item(v)
236+
if result:
237+
return result
238+
return None
239+
240+
return find_item(data)
241+
except json.JSONDecodeError as e:
242+
print(f"Error parsing JSON: {e}")
243+
continue
244+
245+
print("No script with item_closing_action found")
246+
return None

frontend/src/global/fetchData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async function processResponseData(response: Response): Promise<FetchDataResult>
3030
}
3131

3232
export async function fetchData(
33-
method: "POST" | "GET" | "DELETE" | "PUT",
33+
method: "POST" | "GET" | "DELETE" | "PATCH",
3434
path: string,
3535
body?: Record<string, any>,
3636
): Promise<FetchDataResult> {

0 commit comments

Comments
 (0)