habulaj commited on
Commit
40c2ed4
·
verified ·
1 Parent(s): d69e1bc

Delete routers/twitter.py

Browse files
Files changed (1) hide show
  1. routers/twitter.py +0 -502
routers/twitter.py DELETED
@@ -1,502 +0,0 @@
1
- from fastapi import APIRouter, Query, HTTPException
2
- from fastapi.responses import StreamingResponse
3
- from PIL import Image, ImageDraw, ImageFont
4
- from io import BytesIO
5
- import requests
6
- import re
7
- from html import unescape
8
-
9
- router = APIRouter()
10
-
11
- def fetch_tweet_data(tweet_id: str) -> dict:
12
- url = f"https://tweethunter.io/api/thread?tweetId={tweet_id}"
13
- headers = {
14
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0",
15
- "Accept": "application/json",
16
- "Referer": "https://tweethunter.io/tweetpik"
17
- }
18
- try:
19
- resp = requests.get(url, headers=headers, timeout=10)
20
- resp.raise_for_status()
21
- data = resp.json()
22
- if not data:
23
- raise HTTPException(status_code=404, detail="Tweet não encontrado")
24
- return data[0]
25
- except Exception as e:
26
- raise HTTPException(status_code=400, detail=f"Erro ao buscar tweet: {e}")
27
-
28
- def download_emoji(emoji_url: str) -> Image.Image:
29
- try:
30
- response = requests.get(emoji_url, timeout=10)
31
- response.raise_for_status()
32
- emoji_img = Image.open(BytesIO(response.content)).convert("RGBA")
33
- return emoji_img.resize((32, 32), Image.Resampling.LANCZOS)
34
- except Exception as e:
35
- print(f"Erro ao baixar emoji {emoji_url}: {e}")
36
- return None
37
-
38
- def clean_tweet_text(text: str) -> str:
39
- if not text:
40
- return ""
41
-
42
- text = re.sub(r'<a[^>]*>pic\.x\.com/[^<]*</a>', '', text)
43
-
44
- text = re.sub(r'<img[^>]*alt="([^"]*)"[^>]*/?>', r'\1', text)
45
-
46
- text = re.sub(r'<[^>]+>', '', text)
47
-
48
- text = unescape(text)
49
-
50
- text = text.replace('\\n', '\n')
51
-
52
- text = re.sub(r'\n\s*\n', '\n\n', text)
53
- text = text.strip()
54
-
55
- return text
56
-
57
- def extract_emojis_from_html(text: str) -> list:
58
- emoji_pattern = r'<img[^>]*class="emoji"[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*/?>'
59
- emojis = []
60
-
61
- for match in re.finditer(emoji_pattern, text):
62
- emoji_char = match.group(1)
63
- emoji_url = match.group(2)
64
- start_pos = match.start()
65
- end_pos = match.end()
66
- emojis.append({
67
- 'char': emoji_char,
68
- 'url': emoji_url,
69
- 'start': start_pos,
70
- 'end': end_pos
71
- })
72
-
73
- return emojis
74
-
75
- def wrap_text_with_emojis(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list:
76
- emojis = extract_emojis_from_html(text)
77
- clean_text = clean_tweet_text(text)
78
-
79
- paragraphs = clean_text.split('\n')
80
- all_lines = []
81
- emoji_positions = []
82
- current_char_index = 0
83
-
84
- for paragraph in paragraphs:
85
- if not paragraph.strip():
86
- all_lines.append({
87
- 'text': "",
88
- 'emojis': []
89
- })
90
- current_char_index += 1
91
- continue
92
-
93
- words = paragraph.split()
94
- current_line = ""
95
- line_emojis = []
96
-
97
- for word in words:
98
- test_line = f"{current_line} {word}".strip()
99
-
100
- emoji_count_in_word = 0
101
- for emoji in emojis:
102
- if emoji['char'] in word:
103
- emoji_count_in_word += len(emoji['char'])
104
-
105
- text_width = draw.textlength(test_line, font=font)
106
- emoji_width = emoji_count_in_word * 32
107
- total_width = text_width + emoji_width
108
-
109
- if total_width <= max_width:
110
- current_line = test_line
111
- for emoji in emojis:
112
- if emoji['char'] in word:
113
- emoji_pos_in_line = len(current_line) - len(word) + word.find(emoji['char'])
114
- line_emojis.append({
115
- 'emoji': emoji,
116
- 'position': emoji_pos_in_line
117
- })
118
- else:
119
- if current_line:
120
- all_lines.append({
121
- 'text': current_line,
122
- 'emojis': line_emojis.copy()
123
- })
124
- current_line = word
125
- line_emojis = []
126
- for emoji in emojis:
127
- if emoji['char'] in word:
128
- emoji_pos_in_line = word.find(emoji['char'])
129
- line_emojis.append({
130
- 'emoji': emoji,
131
- 'position': emoji_pos_in_line
132
- })
133
-
134
- if current_line:
135
- all_lines.append({
136
- 'text': current_line,
137
- 'emojis': line_emojis.copy()
138
- })
139
-
140
- current_char_index += len(paragraph) + 1
141
-
142
- return all_lines
143
-
144
- def wrap_text_with_newlines(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]:
145
- paragraphs = text.split('\n')
146
- all_lines = []
147
-
148
- for paragraph in paragraphs:
149
- if not paragraph.strip():
150
- all_lines.append("")
151
- continue
152
-
153
- words = paragraph.split()
154
- current_line = ""
155
-
156
- for word in words:
157
- test_line = f"{current_line} {word}".strip()
158
- if draw.textlength(test_line, font=font) <= max_width:
159
- current_line = test_line
160
- else:
161
- if current_line:
162
- all_lines.append(current_line)
163
- current_line = word
164
-
165
- if current_line:
166
- all_lines.append(current_line)
167
-
168
- return all_lines
169
-
170
- def download_and_resize_image(url: str, max_width: int, max_height: int) -> Image.Image:
171
- try:
172
- response = requests.get(url, timeout=10)
173
- response.raise_for_status()
174
-
175
- img = Image.open(BytesIO(response.content)).convert("RGB")
176
-
177
- original_width, original_height = img.size
178
- ratio = min(max_width / original_width, max_height / original_height)
179
-
180
- new_width = int(original_width * ratio)
181
- new_height = int(original_height * ratio)
182
-
183
- return img.resize((new_width, new_height), Image.Resampling.LANCZOS)
184
- except Exception as e:
185
- print(f"Erro ao baixar imagem {url}: {e}")
186
- return None
187
-
188
- def create_verification_badge(draw: ImageDraw.Draw, x: int, y: int, size: int = 24):
189
- blue_color = (27, 149, 224)
190
-
191
- draw.ellipse((x, y, x + size, y + size), fill=blue_color)
192
-
193
- check_points = [
194
- (x + size * 0.25, y + size * 0.5),
195
- (x + size * 0.45, y + size * 0.7),
196
- (x + size * 0.75, y + size * 0.3)
197
- ]
198
-
199
- line_width = max(2, size // 12)
200
- for i in range(len(check_points) - 1):
201
- draw.line([check_points[i], check_points[i + 1]], fill=(255, 255, 255), width=line_width)
202
-
203
- def format_number(num: int) -> str:
204
- if num >= 1000000:
205
- return f"{num / 1000000:.1f}M"
206
- elif num >= 1000:
207
- return f"{num / 1000:.1f}K"
208
- else:
209
- return str(num)
210
-
211
- def draw_rounded_rectangle(draw: ImageDraw.Draw, bbox: tuple, radius: int, fill: tuple):
212
- x1, y1, x2, y2 = bbox
213
-
214
- draw.rectangle((x1 + radius, y1, x2 - radius, y2), fill=fill)
215
- draw.rectangle((x1, y1 + radius, x2, y2 - radius), fill=fill)
216
-
217
- draw.pieslice((x1, y1, x1 + 2*radius, y1 + 2*radius), 180, 270, fill=fill)
218
- draw.pieslice((x2 - 2*radius, y1, x2, y1 + 2*radius), 270, 360, fill=fill)
219
- draw.pieslice((x1, y2 - 2*radius, x1 + 2*radius, y2), 90, 180, fill=fill)
220
- draw.pieslice((x2 - 2*radius, y2 - 2*radius, x2, y2), 0, 90, fill=fill)
221
-
222
- def draw_rounded_image(img: Image.Image, photo_img: Image.Image, x: int, y: int, radius: int = 16):
223
- mask = Image.new("L", photo_img.size, 0)
224
- mask_draw = ImageDraw.Draw(mask)
225
- mask_draw.rounded_rectangle((0, 0, photo_img.width, photo_img.height), radius, fill=255)
226
-
227
- rounded_img = Image.new("RGBA", photo_img.size, (0, 0, 0, 0))
228
- rounded_img.paste(photo_img, (0, 0))
229
- rounded_img.putalpha(mask)
230
-
231
- img.paste(rounded_img, (x, y), rounded_img)
232
-
233
- def create_tweet_image(tweet: dict) -> BytesIO:
234
- WIDTH, HEIGHT = 1080, 1350
235
-
236
- OUTER_BG_COLOR = (0, 0, 0)
237
- INNER_BG_COLOR = (255, 255, 255)
238
- TEXT_COLOR = (2, 6, 23)
239
- SECONDARY_COLOR = (100, 116, 139)
240
- STATS_COLOR = (110, 118, 125)
241
-
242
- OUTER_PADDING = 64
243
- INNER_PADDING = 48
244
- BORDER_RADIUS = 32
245
- AVATAR_SIZE = 96
246
-
247
- raw_text = tweet.get("textHtml", "")
248
- cleaned_text = clean_tweet_text(raw_text)
249
- photos = tweet.get("photos", [])
250
- videos = tweet.get("videos", [])
251
-
252
- media_url = None
253
- if videos and videos[0].get("poster"):
254
- media_url = videos[0]["poster"]
255
- elif photos:
256
- media_url = photos[0]
257
-
258
- has_media = media_url is not None
259
-
260
- base_font_size = 40
261
- max_iterations = 10
262
- current_iteration = 0
263
-
264
- while current_iteration < max_iterations:
265
- try:
266
- font_name = ImageFont.truetype("fonts/Chirp Bold.woff", int(base_font_size * 0.9))
267
- font_handle = ImageFont.truetype("fonts/Chirp Regular.woff", int(base_font_size * 0.9))
268
- font_text = ImageFont.truetype("fonts/Chirp Regular.woff", base_font_size)
269
- font_stats_number = ImageFont.truetype("fonts/Chirp Bold.woff", int(base_font_size * 0.9))
270
- font_stats_label = ImageFont.truetype("fonts/Chirp Regular.woff", int(base_font_size * 0.9))
271
- except:
272
- font_name = ImageFont.load_default()
273
- font_handle = ImageFont.load_default()
274
- font_text = ImageFont.load_default()
275
- font_stats_number = ImageFont.load_default()
276
- font_stats_label = ImageFont.load_default()
277
-
278
- text_max_width = WIDTH - (2 * OUTER_PADDING) - (2 * INNER_PADDING)
279
-
280
- temp_img = Image.new("RGB", (100, 100))
281
- temp_draw = ImageDraw.Draw(temp_img)
282
-
283
- has_emojis = '<img' in raw_text and 'emoji' in raw_text
284
-
285
- if has_emojis:
286
- lines = wrap_text_with_emojis(raw_text, font_text, text_max_width - 100, temp_draw)
287
- else:
288
- text_lines = wrap_text_with_newlines(cleaned_text, font_text, text_max_width, temp_draw)
289
- lines = [{'text': line, 'emojis': []} for line in text_lines]
290
-
291
- line_height = int(font_text.size * 1.2)
292
- text_height = len(lines) * line_height
293
-
294
- media_height = 0
295
- media_margin = 0
296
- if has_media:
297
- if len(cleaned_text) > 200:
298
- media_height = 250
299
- elif len(cleaned_text) > 100:
300
- media_height = 350
301
- else:
302
- media_height = 450
303
- media_margin = 24
304
-
305
- header_height = AVATAR_SIZE + 16
306
- text_margin = 20
307
- stats_height = 40
308
- stats_margin = 32
309
-
310
- total_content_height = (
311
- INNER_PADDING +
312
- header_height +
313
- text_margin +
314
- text_height +
315
- (media_margin if has_media else 0) +
316
- media_height +
317
- (media_margin if has_media else 0) +
318
- stats_margin +
319
- stats_height +
320
- INNER_PADDING
321
- )
322
-
323
- max_card_height = HEIGHT - (2 * OUTER_PADDING)
324
-
325
- if total_content_height <= max_card_height or base_font_size <= 24:
326
- break
327
-
328
- base_font_size -= 2
329
- current_iteration += 1
330
-
331
- card_height = min(total_content_height, HEIGHT - (2 * OUTER_PADDING))
332
- card_width = WIDTH - (2 * OUTER_PADDING)
333
-
334
- card_x = OUTER_PADDING
335
- card_y = (HEIGHT - card_height) // 2 - 30
336
-
337
- img = Image.new("RGB", (WIDTH, HEIGHT), OUTER_BG_COLOR)
338
- draw = ImageDraw.Draw(img)
339
-
340
- draw_rounded_rectangle(
341
- draw,
342
- (card_x, card_y, card_x + card_width, card_y + card_height),
343
- BORDER_RADIUS,
344
- INNER_BG_COLOR
345
- )
346
-
347
- content_x = card_x + INNER_PADDING
348
- current_y = card_y + INNER_PADDING
349
-
350
- avatar_y = current_y
351
- try:
352
- avatar_resp = requests.get(tweet["avatarUrl"], timeout=10)
353
- avatar_img = Image.open(BytesIO(avatar_resp.content)).convert("RGBA")
354
- avatar_img = avatar_img.resize((AVATAR_SIZE, AVATAR_SIZE), Image.Resampling.LANCZOS)
355
-
356
- mask = Image.new("L", (AVATAR_SIZE, AVATAR_SIZE), 0)
357
- mask_draw = ImageDraw.Draw(mask)
358
- mask_draw.ellipse((0, 0, AVATAR_SIZE, AVATAR_SIZE), fill=255)
359
-
360
- img.paste(avatar_img, (content_x, avatar_y), mask)
361
- except:
362
- draw.ellipse(
363
- (content_x, avatar_y, content_x + AVATAR_SIZE, avatar_y + AVATAR_SIZE),
364
- fill=(200, 200, 200)
365
- )
366
-
367
- user_info_x = content_x + AVATAR_SIZE + 20
368
- user_info_y = avatar_y
369
-
370
- name = tweet.get("nameHtml", "Nome Desconhecido")
371
- name = clean_tweet_text(name)
372
- draw.text((user_info_x, user_info_y), name, font=font_name, fill=TEXT_COLOR)
373
-
374
- verified = tweet.get("verified", False)
375
- if verified:
376
- name_width = draw.textlength(name, font=font_name)
377
- badge_x = user_info_x + name_width + 14
378
- badge_y = user_info_y + 6
379
- create_verification_badge(draw, badge_x, badge_y, 28)
380
-
381
- handle = tweet.get("handler", "@unknown")
382
- if not handle.startswith('@'):
383
- handle = f"@{handle}"
384
-
385
- handle_y = user_info_y + 44
386
- draw.text((user_info_x, handle_y), handle, font=font_handle, fill=SECONDARY_COLOR)
387
-
388
- current_y = avatar_y + header_height + text_margin
389
-
390
- for line_data in lines:
391
- line_text = line_data['text']
392
- line_emojis = line_data.get('emojis', [])
393
-
394
- if line_text.strip() or line_emojis:
395
- text_x = content_x
396
-
397
- if has_emojis and line_emojis:
398
- current_x = text_x
399
- text_parts = []
400
- last_pos = 0
401
-
402
- sorted_emojis = sorted(line_emojis, key=lambda e: e['position'])
403
-
404
- for emoji_data in sorted_emojis:
405
- emoji_pos = emoji_data['position']
406
- emoji_info = emoji_data['emoji']
407
-
408
- if emoji_pos > last_pos:
409
- text_before = line_text[last_pos:emoji_pos]
410
- if text_before:
411
- draw.text((current_x, current_y), text_before, font=font_text, fill=TEXT_COLOR)
412
- current_x += draw.textlength(text_before, font=font_text)
413
-
414
- emoji_img = download_emoji(emoji_info['url'])
415
- if emoji_img:
416
- emoji_y = current_y + (line_height - 32) // 2
417
- img.paste(emoji_img, (int(current_x), int(emoji_y)), emoji_img)
418
- current_x += 32
419
- else:
420
- draw.text((current_x, current_y), emoji_info['char'], font=font_text, fill=TEXT_COLOR)
421
- current_x += draw.textlength(emoji_info['char'], font=font_text)
422
-
423
- last_pos = emoji_pos + len(emoji_info['char'])
424
-
425
- if last_pos < len(line_text):
426
- remaining_text = line_text[last_pos:]
427
- draw.text((current_x, current_y), remaining_text, font=font_text, fill=TEXT_COLOR)
428
- else:
429
- draw.text((text_x, current_y), line_text, font=font_text, fill=TEXT_COLOR)
430
-
431
- current_y += line_height
432
-
433
- if has_media:
434
- current_y += media_margin
435
- media_img = download_and_resize_image(media_url, text_max_width, media_height)
436
-
437
- if media_img:
438
- media_x = content_x
439
- media_y = current_y
440
-
441
- draw_rounded_image(img, media_img, media_x, media_y, 16)
442
- current_y = media_y + media_img.height + media_margin
443
-
444
- current_y += stats_margin
445
- stats_y = current_y
446
- stats_x = content_x
447
-
448
- retweets = tweet.get("retweets", 0)
449
- retweets_text = format_number(retweets)
450
- draw.text((stats_x, stats_y), retweets_text, font=font_stats_number, fill=TEXT_COLOR)
451
-
452
- retweets_num_width = draw.textlength(retweets_text, font=font_stats_number)
453
- retweets_label_x = stats_x + retweets_num_width + 12
454
- draw.text((retweets_label_x, stats_y), "Retweets", font=font_stats_label, fill=STATS_COLOR)
455
-
456
- retweets_label_width = draw.textlength("Retweets", font=font_stats_label)
457
- likes_x = retweets_label_x + retweets_label_width + 44
458
-
459
- likes = tweet.get("likes", 0)
460
- likes_text = format_number(likes)
461
- draw.text((likes_x, stats_y), likes_text, font=font_stats_number, fill=TEXT_COLOR)
462
-
463
- likes_num_width = draw.textlength(likes_text, font=font_stats_number)
464
- likes_label_x = likes_x + likes_num_width + 12
465
- draw.text((likes_label_x, stats_y), "Likes", font=font_stats_label, fill=STATS_COLOR)
466
-
467
- try:
468
- logo_path = "recurve.png"
469
- logo = Image.open(logo_path).convert("RGBA")
470
- logo_width, logo_height = 121, 23
471
- logo_resized = logo.resize((logo_width, logo_height))
472
-
473
- logo_with_opacity = Image.new("RGBA", logo_resized.size)
474
- for x in range(logo_resized.width):
475
- for y in range(logo_resized.height):
476
- r, g, b, a = logo_resized.getpixel((x, y))
477
- new_alpha = int(a * 0.42)
478
- logo_with_opacity.putpixel((x, y), (r, g, b, new_alpha))
479
-
480
- logo_x = WIDTH - logo_width - 64
481
- logo_y = HEIGHT - logo_height - 64
482
- img.paste(logo_with_opacity, (logo_x, logo_y), logo_with_opacity)
483
- except Exception as e:
484
- print(f"Erro ao carregar a logo: {e}")
485
-
486
- buffer = BytesIO()
487
- img.save(buffer, format="PNG", quality=95)
488
- buffer.seek(0)
489
- return buffer
490
-
491
- def extract_tweet_id(tweet_url: str) -> str:
492
- match = re.search(r"/status/(\d+)", tweet_url)
493
- if not match:
494
- raise HTTPException(status_code=400, detail="URL de tweet inválida")
495
- return match.group(1)
496
-
497
- @router.get("/tweet/image")
498
- def get_tweet_image(tweet_url: str = Query(..., description="URL do tweet")):
499
- tweet_id = extract_tweet_id(tweet_url)
500
- tweet_data = fetch_tweet_data(tweet_id)
501
- img_buffer = create_tweet_image(tweet_data)
502
- return StreamingResponse(img_buffer, media_type="image/png")