Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,7 +15,7 @@ CSV_FILE = 'endpoints.csv'
|
|
| 15 |
# -------------------------------------------------------------------------
|
| 16 |
|
| 17 |
def load_data():
|
| 18 |
-
"""Loads the endpoints from the CSV file."""
|
| 19 |
if not os.path.exists(CSV_FILE):
|
| 20 |
return []
|
| 21 |
|
|
@@ -32,37 +32,22 @@ def load_data():
|
|
| 32 |
return []
|
| 33 |
|
| 34 |
def get_type1_choices():
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
if not data:
|
| 38 |
-
return []
|
| 39 |
-
return list(set([item.get('TYPE1') for item in data if item.get('TYPE1')]))
|
| 40 |
|
| 41 |
def get_type2_choices(selected_type1):
|
| 42 |
-
"
|
| 43 |
-
data = load_data()
|
| 44 |
-
if not data or not selected_type1:
|
| 45 |
-
return []
|
| 46 |
-
|
| 47 |
-
choices = [item.get('TYPE2') for item in data if item.get('TYPE1') == selected_type1]
|
| 48 |
-
unique_choices = list(set(choices))
|
| 49 |
-
unique_choices.sort()
|
| 50 |
-
|
| 51 |
-
all_option = f"ALL {selected_type1.upper()} REPORTS"
|
| 52 |
-
return [all_option] + unique_choices
|
| 53 |
|
| 54 |
# -------------------------------------------------------------------------
|
| 55 |
# ROBUST LINK RESOLVER
|
| 56 |
# -------------------------------------------------------------------------
|
| 57 |
|
| 58 |
def resolve_tiktok_link(url):
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
(This function remains unchanged as it resolves IDs, not sends the report)
|
| 62 |
-
"""
|
| 63 |
if not url:
|
| 64 |
return "", "", "", "", "β Error: Please enter a URL first."
|
| 65 |
-
|
| 66 |
headers = {
|
| 67 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 68 |
"Referer": "https://www.tiktok.com/",
|
|
@@ -166,39 +151,23 @@ def resolve_tiktok_link(url):
|
|
| 166 |
except Exception as e:
|
| 167 |
return "", "", "", "", f"β Network Error: {str(e)}"
|
| 168 |
|
|
|
|
| 169 |
# -------------------------------------------------------------------------
|
| 170 |
# REPORT EXECUTION LOGIC
|
| 171 |
# -------------------------------------------------------------------------
|
| 172 |
|
| 173 |
-
def
|
| 174 |
"""
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
This function should call a third-party service that calculates
|
| 178 |
-
the X-Bogus, msToken, and _signature for the given unsigned_url
|
| 179 |
-
and User-Agent. It should return the fully signed URL.
|
| 180 |
-
|
| 181 |
-
The existing failure (200 OK/Empty Body) is due to the lack of a
|
| 182 |
-
valid X-Bogus matching the new object_id/parameters.
|
| 183 |
-
|
| 184 |
-
Since we cannot implement the signing here, we return the unsigned URL.
|
| 185 |
"""
|
| 186 |
-
|
| 187 |
-
return unsigned_url # Returns the unsigned URL until a signing service is integrated.
|
| 188 |
-
|
| 189 |
-
def patch_url(original_url, new_params):
|
| 190 |
parsed = urlparse(original_url)
|
| 191 |
-
|
| 192 |
-
# 1. Parse all parameters, taking the first value from the list (TikTok uses single values)
|
| 193 |
original_query = parse_qs(parsed.query)
|
|
|
|
| 194 |
# Convert query list values to single string values
|
| 195 |
params = {k: v[0] for k, v in original_query.items()}
|
| 196 |
|
| 197 |
-
#
|
| 198 |
-
# These must be regenerated by an external service
|
| 199 |
-
signature_keys = ['X-Bogus', 'msToken', '_signature', 'X-Gnarly', 'tt_webid', 'tt_webid_v2']
|
| 200 |
-
|
| 201 |
-
# 3. DEFINITIONS: Keys to be REPLACED (Target IDs)
|
| 202 |
target_mapping = {
|
| 203 |
'object_id': new_params.get('object_id'),
|
| 204 |
'owner_id': new_params.get('owner_id'),
|
|
@@ -210,47 +179,36 @@ def patch_url(original_url, new_params):
|
|
| 210 |
'object_owner_id': new_params.get('owner_id'),
|
| 211 |
}
|
| 212 |
|
| 213 |
-
#
|
| 214 |
-
for key in signature_keys:
|
| 215 |
-
if key in params:
|
| 216 |
-
del params[key]
|
| 217 |
-
|
| 218 |
-
# 5. Patch: Overwrite/Set all target IDs
|
| 219 |
for param_key, new_value in target_mapping.items():
|
| 220 |
if new_value and new_value.strip():
|
| 221 |
# Apply the new ID if the key exists OR if it's a common target key
|
| 222 |
params[param_key] = new_value.strip()
|
| 223 |
|
| 224 |
-
|
| 225 |
-
# 6. Rebuild the URL
|
| 226 |
-
# doseq=False ensures parameters are not encoded as lists (e.g., param=a¶m=b)
|
| 227 |
new_query = urlencode(params, doseq=False)
|
| 228 |
|
| 229 |
-
#
|
| 230 |
new_query = new_query.replace('+', '%20')
|
| 231 |
-
# -----------------------------------------------------------------------
|
| 232 |
|
| 233 |
new_url = urlunparse(parsed._replace(query=new_query))
|
| 234 |
|
| 235 |
return new_url
|
| 236 |
|
| 237 |
-
def execute_reports(
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
all_check_str = f"ALL {type1.upper()} REPORTS"
|
| 244 |
-
if type2 == all_check_str:
|
| 245 |
-
targets = [item for item in data if item.get('TYPE1') == type1]
|
| 246 |
-
yield f"π Preparing ALL {len(targets)} reports for: {type1}...\n"
|
| 247 |
-
else:
|
| 248 |
-
targets = [item for item in data if item.get('TYPE1') == type1 and item.get('TYPE2') == type2]
|
| 249 |
-
yield f"π Preparing specific report: {type2}...\n"
|
| 250 |
-
|
| 251 |
-
if not targets:
|
| 252 |
-
yield "β No matching endpoints found in CSV."
|
| 253 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
custom_target_data = {
|
| 256 |
'object_id': obj_id,
|
|
@@ -258,6 +216,9 @@ def execute_reports(type1, type2, obj_id, own_id, sec_uid, nick):
|
|
| 258 |
'sec_uid': sec_uid,
|
| 259 |
'nickname': nick
|
| 260 |
}
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
headers = {
|
| 263 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
@@ -266,34 +227,32 @@ def execute_reports(type1, type2, obj_id, own_id, sec_uid, nick):
|
|
| 266 |
|
| 267 |
log_history = ""
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
if not original_url:
|
| 274 |
-
continue
|
| 275 |
-
|
| 276 |
-
# 1. Patch the URL: Swaps IDs AND removes the old, invalid signatures
|
| 277 |
-
unsigned_url = patch_url(original_url, custom_target_data)
|
| 278 |
|
| 279 |
-
#
|
| 280 |
-
final_url =
|
| 281 |
|
| 282 |
-
#
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
| 286 |
|
|
|
|
|
|
|
|
|
|
| 287 |
try:
|
| 288 |
-
#
|
| 289 |
response = requests.get(final_url, headers=headers)
|
| 290 |
timestamp = time.strftime("%H:%M:%S")
|
| 291 |
|
| 292 |
-
#
|
| 293 |
response_content = ""
|
| 294 |
if not response.text.strip():
|
| 295 |
-
|
| 296 |
-
response_content = "[Empty Response Body] (Likely Signature Mismatch/Rejected)"
|
| 297 |
else:
|
| 298 |
try:
|
| 299 |
parsed = response.json()
|
|
@@ -305,9 +264,9 @@ def execute_reports(type1, type2, obj_id, own_id, sec_uid, nick):
|
|
| 305 |
# Check for 200 and a JSON indicator of success (like "code":0 or "success":true)
|
| 306 |
status_icon = "β
" if response.status_code == 200 and ('success' in response_content or 'code":0' in response_content) else "β οΈ"
|
| 307 |
|
| 308 |
-
#
|
| 309 |
log_entry = (
|
| 310 |
-
f"[{timestamp}] {status_icon} REQ ({i+1}/{len(
|
| 311 |
f" β‘ Sending URL: {final_url}\n"
|
| 312 |
f" β¬ Status: {response.status_code}\n"
|
| 313 |
f" β¬ Reply: {response_content}\n"
|
|
@@ -335,7 +294,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
| 335 |
with gr.Column(scale=1):
|
| 336 |
gr.Markdown("### 1. Target Configuration")
|
| 337 |
with gr.Group():
|
| 338 |
-
link_input = gr.Textbox(label="TikTok Link", placeholder="https://www.tiktok.com/@username...")
|
| 339 |
resolve_btn = gr.Button("π Resolve IDs from Link", size="sm")
|
| 340 |
resolve_status = gr.Markdown("")
|
| 341 |
|
|
@@ -346,14 +305,21 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
| 346 |
with gr.Row():
|
| 347 |
sec_uid_input = gr.Textbox(label="SecUID", info="Starts with MS4w...")
|
| 348 |
nick_input = gr.Textbox(label="Nickname", info="Username")
|
| 349 |
-
|
| 350 |
-
gr.Markdown("---")
|
| 351 |
-
gr.Markdown("### 2. Report Settings")
|
| 352 |
-
t1_input = gr.Dropdown(choices=get_type1_choices(), label="Category")
|
| 353 |
-
t2_input = gr.Dropdown(choices=[], label="Report Type")
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
with gr.Column(scale=1):
|
| 359 |
gr.Markdown("### 3. Execution Logs")
|
|
@@ -368,19 +334,19 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
| 368 |
|
| 369 |
resolve_btn.click(fn=resolve_tiktok_link, inputs=link_input, outputs=[obj_id_input, own_id_input, sec_uid_input, nick_input, resolve_status])
|
| 370 |
|
| 371 |
-
|
| 372 |
-
new_choices = get_type2_choices(choice)
|
| 373 |
-
return gr.Dropdown(choices=new_choices, value=new_choices[0] if new_choices else None)
|
| 374 |
-
|
| 375 |
-
t1_input.change(fn=update_second_dropdown, inputs=t1_input, outputs=t2_input)
|
| 376 |
|
| 377 |
-
send_btn.click(fn=execute_reports,
|
|
|
|
|
|
|
|
|
|
| 378 |
|
|
|
|
| 379 |
def refresh_ui():
|
| 380 |
-
|
| 381 |
-
return gr.Dropdown(choices=
|
| 382 |
|
| 383 |
-
refresh_btn.click(fn=refresh_ui, outputs=[
|
| 384 |
|
| 385 |
if __name__ == "__main__":
|
| 386 |
app.launch()
|
|
|
|
| 15 |
# -------------------------------------------------------------------------
|
| 16 |
|
| 17 |
def load_data():
|
| 18 |
+
"""Loads the endpoints from the CSV file. (Kept for UI compatibility, but unused in new logic)"""
|
| 19 |
if not os.path.exists(CSV_FILE):
|
| 20 |
return []
|
| 21 |
|
|
|
|
| 32 |
return []
|
| 33 |
|
| 34 |
def get_type1_choices():
|
| 35 |
+
# Only returning a placeholder or 'Manual' since the CSV reports are not used
|
| 36 |
+
return ["Manual URL Report"]
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
def get_type2_choices(selected_type1):
|
| 39 |
+
return ["Run Batch"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
# -------------------------------------------------------------------------
|
| 42 |
# ROBUST LINK RESOLVER
|
| 43 |
# -------------------------------------------------------------------------
|
| 44 |
|
| 45 |
def resolve_tiktok_link(url):
|
| 46 |
+
# This remains unchanged
|
| 47 |
+
# ... (existing ID extraction logic) ...
|
|
|
|
|
|
|
| 48 |
if not url:
|
| 49 |
return "", "", "", "", "β Error: Please enter a URL first."
|
| 50 |
+
# ... (rest of resolve_tiktok_link unchanged) ...
|
| 51 |
headers = {
|
| 52 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 53 |
"Referer": "https://www.tiktok.com/",
|
|
|
|
| 151 |
except Exception as e:
|
| 152 |
return "", "", "", "", f"β Network Error: {str(e)}"
|
| 153 |
|
| 154 |
+
|
| 155 |
# -------------------------------------------------------------------------
|
| 156 |
# REPORT EXECUTION LOGIC
|
| 157 |
# -------------------------------------------------------------------------
|
| 158 |
|
| 159 |
+
def patch_master_url(original_url, new_params):
|
| 160 |
"""
|
| 161 |
+
Patches a FULLY SIGNED URL by only replacing the Target IDs
|
| 162 |
+
while keeping all other parameters (including signatures) intact.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
parsed = urlparse(original_url)
|
|
|
|
|
|
|
| 165 |
original_query = parse_qs(parsed.query)
|
| 166 |
+
|
| 167 |
# Convert query list values to single string values
|
| 168 |
params = {k: v[0] for k, v in original_query.items()}
|
| 169 |
|
| 170 |
+
# Keys to be REPLACED (Target IDs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
target_mapping = {
|
| 172 |
'object_id': new_params.get('object_id'),
|
| 173 |
'owner_id': new_params.get('owner_id'),
|
|
|
|
| 179 |
'object_owner_id': new_params.get('owner_id'),
|
| 180 |
}
|
| 181 |
|
| 182 |
+
# Overwrite/Set all target IDs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
for param_key, new_value in target_mapping.items():
|
| 184 |
if new_value and new_value.strip():
|
| 185 |
# Apply the new ID if the key exists OR if it's a common target key
|
| 186 |
params[param_key] = new_value.strip()
|
| 187 |
|
| 188 |
+
# Rebuild the URL
|
|
|
|
|
|
|
| 189 |
new_query = urlencode(params, doseq=False)
|
| 190 |
|
| 191 |
+
# CRITICAL FIX: Replace '+' with '%20' for TikTok URL compatibility
|
| 192 |
new_query = new_query.replace('+', '%20')
|
|
|
|
| 193 |
|
| 194 |
new_url = urlunparse(parsed._replace(query=new_query))
|
| 195 |
|
| 196 |
return new_url
|
| 197 |
|
| 198 |
+
def execute_reports(master_url_batch, obj_id, own_id, sec_uid, nick, *args):
|
| 199 |
+
"""
|
| 200 |
+
Executes a batch of reports based on the multiple signed URLs provided by the user.
|
| 201 |
+
"""
|
| 202 |
+
if not master_url_batch or not master_url_batch.strip():
|
| 203 |
+
yield "β Error: Master URL Batch is empty. Please paste one or more full, working report URLs."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
return
|
| 205 |
+
|
| 206 |
+
# Split the multi-line input into a list of URLs
|
| 207 |
+
master_urls = [u.strip() for u in master_url_batch.split('\n') if u.strip()]
|
| 208 |
+
|
| 209 |
+
if not master_urls:
|
| 210 |
+
yield "β Error: No valid URLs found in the Master URL Batch input."
|
| 211 |
+
return
|
| 212 |
|
| 213 |
custom_target_data = {
|
| 214 |
'object_id': obj_id,
|
|
|
|
| 216 |
'sec_uid': sec_uid,
|
| 217 |
'nickname': nick
|
| 218 |
}
|
| 219 |
+
|
| 220 |
+
if not obj_id or not own_id or not sec_uid or not nick:
|
| 221 |
+
yield "β οΈ WARNING: One or more target IDs are missing. The server may reject requests. Proceeding anyway."
|
| 222 |
|
| 223 |
headers = {
|
| 224 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
|
|
| 227 |
|
| 228 |
log_history = ""
|
| 229 |
|
| 230 |
+
yield f"π Preparing {len(master_urls)} unique report types to send...\n"
|
| 231 |
+
|
| 232 |
+
for i, original_url in enumerate(master_urls):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
# 1. Patch the URL: Swaps Target IDs but retains all original signatures and reason codes.
|
| 235 |
+
final_url = patch_master_url(original_url, custom_target_data)
|
| 236 |
|
| 237 |
+
# Attempt to extract report reason for logging clarity
|
| 238 |
+
try:
|
| 239 |
+
reason_code = parse_qs(urlparse(original_url).query).get('reason', ['UNKNOWN'])[0]
|
| 240 |
+
report_name = f"Reason: {reason_code} / Type: {parse_qs(urlparse(original_url).query).get('report_type', ['user'])[0]}"
|
| 241 |
+
except:
|
| 242 |
+
report_name = f"Report {i+1}"
|
| 243 |
|
| 244 |
+
# Log a warning that we're hoping the server doesn't notice the ID swap
|
| 245 |
+
log_history += f"[{time.strftime('%H:%M:%S')}] π‘ NOTE: Attempting to swap IDs with original URL's signature.\n"
|
| 246 |
+
|
| 247 |
try:
|
| 248 |
+
# 2. Send the Request
|
| 249 |
response = requests.get(final_url, headers=headers)
|
| 250 |
timestamp = time.strftime("%H:%M:%S")
|
| 251 |
|
| 252 |
+
# 3. Detailed Response Logging
|
| 253 |
response_content = ""
|
| 254 |
if not response.text.strip():
|
| 255 |
+
response_content = "[Empty Response Body] (Likely Signature/ID Mismatch)"
|
|
|
|
| 256 |
else:
|
| 257 |
try:
|
| 258 |
parsed = response.json()
|
|
|
|
| 264 |
# Check for 200 and a JSON indicator of success (like "code":0 or "success":true)
|
| 265 |
status_icon = "β
" if response.status_code == 200 and ('success' in response_content or 'code":0' in response_content) else "β οΈ"
|
| 266 |
|
| 267 |
+
# 4. Build Log Entry with URL
|
| 268 |
log_entry = (
|
| 269 |
+
f"[{timestamp}] {status_icon} REQ ({i+1}/{len(master_urls)}): {report_name}\n"
|
| 270 |
f" β‘ Sending URL: {final_url}\n"
|
| 271 |
f" β¬ Status: {response.status_code}\n"
|
| 272 |
f" β¬ Reply: {response_content}\n"
|
|
|
|
| 294 |
with gr.Column(scale=1):
|
| 295 |
gr.Markdown("### 1. Target Configuration")
|
| 296 |
with gr.Group():
|
| 297 |
+
link_input = gr.Textbox(label="TikTok Link for ID Resolution", placeholder="https://www.tiktok.com/@username...")
|
| 298 |
resolve_btn = gr.Button("π Resolve IDs from Link", size="sm")
|
| 299 |
resolve_status = gr.Markdown("")
|
| 300 |
|
|
|
|
| 305 |
with gr.Row():
|
| 306 |
sec_uid_input = gr.Textbox(label="SecUID", info="Starts with MS4w...")
|
| 307 |
nick_input = gr.Textbox(label="Nickname", info="Username")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
+
gr.Markdown("---")
|
| 310 |
+
gr.Markdown("### 2. Master URL Batch Input")
|
| 311 |
+
# NEW: Multi-line input for Master URLs
|
| 312 |
+
master_url_batch_input = gr.Textbox(
|
| 313 |
+
label="Master Signed Report URLs (One per line)",
|
| 314 |
+
placeholder="Paste ONE OR MORE FULLY SIGNED & working report URLs here. (Profile, Video, or Comment reports).",
|
| 315 |
+
lines=5,
|
| 316 |
+
max_lines=10
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# Removed Type 1/Type 2 selection as it's no longer used
|
| 320 |
+
# We keep the send button but link it to the new inputs
|
| 321 |
+
send_btn = gr.Button("π Send Report Batch", variant="primary", size="lg")
|
| 322 |
+
refresh_btn = gr.Button("π Reload CSV (Data not used)", variant="secondary")
|
| 323 |
|
| 324 |
with gr.Column(scale=1):
|
| 325 |
gr.Markdown("### 3. Execution Logs")
|
|
|
|
| 334 |
|
| 335 |
resolve_btn.click(fn=resolve_tiktok_link, inputs=link_input, outputs=[obj_id_input, own_id_input, sec_uid_input, nick_input, resolve_status])
|
| 336 |
|
| 337 |
+
# We remove the dependency on the Type 1/Type 2 dropdowns
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
+
send_btn.click(fn=execute_reports,
|
| 340 |
+
# Pass the new batch input, and the 4 ID inputs
|
| 341 |
+
inputs=[master_url_batch_input, obj_id_input, own_id_input, sec_uid_input, nick_input],
|
| 342 |
+
outputs=logs_output)
|
| 343 |
|
| 344 |
+
# Keeping refresh button mostly for aesthetics, as CSV data is not relevant now
|
| 345 |
def refresh_ui():
|
| 346 |
+
# Only refresh the dropdowns to reflect the simplified UI state
|
| 347 |
+
return gr.Dropdown(choices=["Manual URL Report"]), gr.Dropdown(choices=["Run Batch"], value="Run Batch")
|
| 348 |
|
| 349 |
+
refresh_btn.click(fn=refresh_ui, outputs=[]) # No outputs to refresh the manual setup
|
| 350 |
|
| 351 |
if __name__ == "__main__":
|
| 352 |
app.launch()
|