File size: 8,228 Bytes
c4f0411 2e6a8b3 c4f0411 837227b 1303dea 2ce54f6 2e6a8b3 cd0a3ca c4f0411 1303dea e5c3fa4 cd0a3ca c4f0411 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 95d465d eee505e 95d465d eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 eee505e 2e6a8b3 1303dea e5c3fa4 1303dea e5c3fa4 1303dea e5c3fa4 2ce54f6 e5c3fa4 2ce54f6 e5c3fa4 2ce54f6 837227b 1303dea 2ce54f6 837227b 2ce54f6 837227b 2ce54f6 837227b 2ce54f6 837227b 1303dea 2ce54f6 1303dea 2ce54f6 1303dea 2ce54f6 1303dea c4f0411 2ce54f6 1303dea |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from playwright.async_api import async_playwright
from typing import Optional
import tempfile
import os
import httpx
app = FastAPI()
# Configura CORS para permitir requisições do frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Em produção, especifique os domínios permitidos
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Modelo para criar posts (gerar imagem a partir de HTML)
class PostRequest(BaseModel):
html: str
width: Optional[int] = 1080 # Largura padrão para posts (Instagram)
height: Optional[int] = 1350 # Altura padrão para posts (Instagram)
@app.get("/")
def greet_json():
return {"Hello": "World!"}
# Cookies para a API do TweetHunter
TWEETHUNTER_COOKIES = "_ga_F7Q3BYCL54=GS2.1.s1767919580$o2$g1$t1767919660$j60$l0$h0; _ga=GA1.1.1193193650.1767916115; _fbp=fb.1.1767916115096.41407626306959845; _gid=GA1.2.1486115979.1767916116; _gcl_au=1.1.249737643.1767919602; _ga_BH3EC90N3P=GS2.1.s1767919602$o1$g0$t1767919645$j17$l0$h0; rl_anonymous_id=RS_ENC_v3_IjYxMDgxYWQ0LTQzZWItNDI2MS1iM2NiLWU1Y2Y1OTE4YzdmYyI%3D; rl_page_init_referrer=RS_ENC_v3_IiRkaXJlY3Qi; rl_session=RS_ENC_v3_eyJpZCI6MTc2NzkxOTYwNDUyMSwiZXhwaXJlc0F0IjoxNzY3OTIxNDA0NTIxLCJ0aW1lb3V0IjoxODAwMDAwLCJhdXRvVHJhY2siOnRydWV9; amp_724fdb=5ELVqtiBnGz_OOimEKaKlK...1jeg3gla6.1jeg3gla6.0.0.0; __Host-next-auth.csrf-token=a96665733add52ae5bec69d6cc7ed672a625be6af758e630f11c4238c9bf0dc7%7C803d48ffeb4850440908a7f81faa1eb53093aa583f77d9ae9bcb5f2d7b324b00; __Secure-next-auth.callback-url=https%3A%2F%2Fapp.tweethunter.io; amp_724fdb_tweethunter.io=5ELVqtiBnGz_OOimEKaKlK...1jeg3gla6.1jeg3glo0.0.0.0"
@app.get("/api/tweet/{tweet_id}")
async def get_tweet_data(tweet_id: str):
"""
Busca dados completos de um tweet combinando duas APIs:
- react-tweet.vercel.app: metadados (likes, comentários, verified_type, etc.)
- tweethunter.io: texto completo do tweet
"""
try:
# URLs das duas APIs
metadata_url = f"https://react-tweet.vercel.app/api/tweet/{tweet_id}"
fulltext_url = f"https://tweethunter.io/api/thread?tweetId={tweet_id}"
tweethunter_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0",
"Accept": "application/json",
"Accept-Language": "pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
"Referer": "https://tweethunter.io/tweetpik",
"Alt-Used": "tweethunter.io",
"Connection": "keep-alive",
"Cookie": TWEETHUNTER_COOKIES,
}
async with httpx.AsyncClient() as client:
# Fazer as duas requisições em paralelo
import asyncio
metadata_task = client.get(metadata_url, timeout=10.0)
fulltext_task = client.get(fulltext_url, headers=tweethunter_headers, timeout=10.0)
metadata_response, fulltext_response = await asyncio.gather(
metadata_task,
fulltext_task,
return_exceptions=True
)
# Processar resposta de metadados (obrigatória)
if isinstance(metadata_response, Exception):
raise HTTPException(status_code=500, detail=f"Erro ao buscar metadados: {str(metadata_response)}")
if metadata_response.status_code != 200:
raise HTTPException(
status_code=metadata_response.status_code,
detail=f"API de metadados retornou {metadata_response.status_code}"
)
metadata = metadata_response.json()
# Processar resposta de texto completo (opcional - fallback para texto truncado)
fulltext_html = None
if not isinstance(fulltext_response, Exception) and fulltext_response.status_code == 200:
try:
fulltext_data = fulltext_response.json()
if isinstance(fulltext_data, list) and len(fulltext_data) > 0:
fulltext_html = fulltext_data[0].get("textHtml")
except:
pass
# Processar dados
data = metadata.get("data", {})
# Remover "_normal" da URL do avatar para obter imagem em alta resolução
if "user" in data and "profile_image_url_https" in data["user"]:
avatar_url = data["user"]["profile_image_url_https"]
# Remover _normal antes da extensão (ex: _normal.jpg -> .jpg)
data["user"]["profile_image_url_https"] = avatar_url.replace("_normal.", ".")
# Combinar os dados
result = {
"data": data,
"fullTextHtml": fulltext_html
}
return result
except HTTPException:
raise
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Timeout ao conectar com APIs")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao buscar dados do tweet: {str(e)}")
@app.post("/create-post")
async def create_post(request: PostRequest):
"""
Cria uma imagem a partir de HTML para posts.
Recebe HTML e retorna uma imagem PNG.
Exemplo de uso:
{
"html": "<div>Conteúdo do post</div>",
"width": 1080,
"height": 1350
}
"""
html_file = None
try:
# Garantir que o HTML está completo
html_content = request.html
# Se o HTML não tiver estrutura completa, adicionar
if "<!DOCTYPE" not in html_content and "<html" not in html_content:
html_content = f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
{html_content}
</body>
</html>"""
# Criar arquivo temporário para o HTML
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(html_content)
html_file = f.name
try:
# Usar Playwright Async API para renderizar HTML e gerar screenshot
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(viewport={'width': request.width, 'height': request.height})
# Carrega o HTML do arquivo temporário
await page.goto(f'file://{html_file}')
# Espera o conteúdo carregar (especialmente imagens base64)
await page.wait_for_timeout(2000) # Aguarda 2 segundos para garantir que imagens e fontes carreguem
# Tira screenshot
screenshot_bytes = await page.screenshot(full_page=True, type='png')
await browser.close()
# Remove arquivo temporário
if os.path.exists(html_file):
os.unlink(html_file)
html_file = None
# Retorna a imagem
return Response(
content=screenshot_bytes,
media_type="image/png",
headers={
"Content-Disposition": "attachment; filename=post.png"
}
)
except Exception as e:
# Remove arquivo temporário em caso de erro
if html_file and os.path.exists(html_file):
os.unlink(html_file)
raise e
except Exception as e:
# Garantir limpeza do arquivo temporário
if html_file and os.path.exists(html_file):
try:
os.unlink(html_file)
except:
pass
raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem do post: {str(e)}") |