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)}")