Spaces:
Running
Running
File size: 14,618 Bytes
4ffe0a9 |
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 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
import os
import httpx
from typing import Dict, Optional, Tuple
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter()
# 📱 Instagram API Config
INSTAGRAM_API_BASE = "https://graph.instagram.com/v23.0"
INSTAGRAM_PAGE_ID = "17841464166934843" # Seu Page ID
INSTAGRAM_TOKEN = os.getenv("INSTAGRAM_ACCESS_TOKEN")
if not INSTAGRAM_TOKEN:
raise ValueError("❌ INSTAGRAM_ACCESS_TOKEN não foi definido nas variáveis de ambiente!")
# 📝 Modelos de dados
class InstagramPost(BaseModel):
image_url: str
caption: Optional[str] = None
class PublishResponse(BaseModel):
success: bool
media_id: Optional[str] = None
post_id: Optional[str] = None
post_url: Optional[str] = None
message: str
comment_posted: bool = False
comment_id: Optional[str] = None
def convert_to_bold_unicode(text: str) -> str:
"""
Converte texto normal para caracteres Unicode sans-serif bold.
Usa uma fonte mais próxima da padrão do Instagram.
Funciona para letras A-Z, a-z e números 0-9.
"""
# Mapeamento usando Mathematical Sans-Serif Bold (mais próximo da fonte do Instagram)
bold_map = {
'A': '𝗔', 'B': '𝗕', 'C': '𝗖', 'D': '𝗗', 'E': '𝗘', 'F': '𝗙', 'G': '𝗚', 'H': '𝗛',
'I': '𝗜', 'J': '𝗝', 'K': '𝗞', 'L': '𝗟', 'M': '𝗠', 'N': '𝗡', 'O': '𝗢', 'P': '𝗣',
'Q': '𝗤', 'R': '𝗥', 'S': '𝗦', 'T': '𝗧', 'U': '𝗨', 'V': '𝗩', 'W': '𝗪', 'X': '𝗫',
'Y': '𝗬', 'Z': '𝗭',
'a': '𝗮', 'b': '𝗯', 'c': '𝗰', 'd': '𝗱', 'e': '𝗲', 'f': '𝗳', 'g': '𝗴', 'h': '𝗵',
'i': '𝗶', 'j': '𝗷', 'k': '𝗸', 'l': '𝗹', 'm': '𝗺', 'n': '𝗻', 'o': '𝗼', 'p': '𝗽',
'q': '𝗾', 'r': '𝗿', 's': '𝘀', 't': '𝘁', 'u': '𝘂', 'v': '𝘃', 'w': '𝘄', 'x': '𝘅',
'y': '𝘆', 'z': '𝘇',
'0': '𝟬', '1': '𝟭', '2': '𝟮', '3': '𝟯', '4': '𝟰', '5': '𝟱', '6': '𝟲', '7': '𝟳',
'8': '𝟴', '9': '𝟵'
}
return ''.join(bold_map.get(char, char) for char in text)
def convert_to_italic_unicode(text: str) -> str:
"""
Converte texto normal para caracteres Unicode sans-serif itálico.
Usa uma fonte mais próxima da padrão do Instagram.
Funciona para letras A-Z, a-z e números 0-9.
"""
# Mapeamento usando Mathematical Sans-Serif Italic (mais próximo da fonte do Instagram)
italic_map = {
'A': '𝘈', 'B': '𝘉', 'C': '𝘊', 'D': '𝘋', 'E': '𝘌', 'F': '𝘍', 'G': '𝘎', 'H': '𝘏',
'I': '𝘐', 'J': '𝘑', 'K': '𝘒', 'L': '𝘓', 'M': '𝘔', 'N': '𝘕', 'O': '𝘖', 'P': '𝘗',
'Q': '𝘘', 'R': '𝘙', 'S': '𝘚', 'T': '𝘛', 'U': '𝘜', 'V': '𝘝', 'W': '𝘞', 'X': '𝘟',
'Y': '𝘠', 'Z': '𝘡',
'a': '𝘢', 'b': '𝘣', 'c': '𝘤', 'd': '𝘥', 'e': '𝘦', 'f': '𝘧', 'g': '𝘨', 'h': '𝘩',
'i': '𝘪', 'j': '𝘫', 'k': '𝘬', 'l': '𝘭', 'm': '𝘮', 'n': '𝘯', 'o': '𝘰', 'p': '𝘱',
'q': '𝘲', 'r': '𝘳', 's': '𝘴', 't': '𝘵', 'u': '𝘶', 'v': '𝘷', 'w': '𝘸', 'x': '𝘹',
'y': '𝘺', 'z': '𝘻',
# Números itálicos (mesmo conjunto do negrito, pois não há itálico específico)
'0': '𝟬', '1': '𝟭', '2': '𝟮', '3': '𝟯', '4': '𝟰', '5': '𝟱', '6': '𝟲', '7': '𝟳',
'8': '𝟴', '9': '𝟵'
}
return ''.join(italic_map.get(char, char) for char in text)
def clean_html_tags(text: str) -> str:
"""
Remove todas as tags HTML exceto <strong> e <em>.
Converte <h2> para <strong> (negrito).
Converte <p> para quebras de linha.
Remove completamente outras tags como <li>, <h3>, etc.
Returns:
str: Texto limpo com apenas <strong> e <em>
"""
if not text:
return ""
import re
# Converte <h2> para <strong> (mantendo o conteúdo)
text = re.sub(r'<h2[^>]*>(.*?)</h2>', r'<strong>\1</strong>', text, flags=re.DOTALL | re.IGNORECASE)
# Converte <p> para quebras de linha
text = re.sub(r'<p[^>]*>(.*?)</p>', r'\1\n\n', text, flags=re.DOTALL | re.IGNORECASE)
# Converte <br> e <br/> para quebra de linha simples
text = re.sub(r'<br\s*/?>', '\n', text, flags=re.IGNORECASE)
# Remove todas as outras tags HTML, mantendo apenas o conteúdo
# Exclui <strong>, </strong>, <em>, </em> da remoção
text = re.sub(r'<(?!/?(?:strong|em)\b)[^>]*>', '', text, flags=re.IGNORECASE)
# Remove quebras de linha excessivas e espaços extras
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text) # Máximo de 2 quebras consecutivas
text = re.sub(r'[ \t]+', ' ', text) # Remove espaços/tabs extras
text = text.strip()
return text
def format_text_for_instagram(text: str) -> Tuple[str, Optional[str]]:
"""
Formata o texto para o Instagram:
1. Limpa tags HTML indesejadas
2. Converte <strong> para negrito Unicode
3. Converte <em> para itálico Unicode
4. Corta se necessário e retorna o texto principal e o resto para comentário
Returns:
Tuple[str, Optional[str]]: (texto_principal, resto_para_comentario)
"""
if not text:
return "", None
# 🧹 Primeiro, limpa as tags HTML indesejadas
text = clean_html_tags(text)
# 🔤 Converte tags <strong> para negrito Unicode do Instagram
import re
def replace_strong_tags(match):
content = match.group(1) # Conteúdo entre as tags <strong>
return convert_to_bold_unicode(content)
def replace_em_tags(match):
content = match.group(1) # Conteúdo entre as tags <em>
return convert_to_italic_unicode(content)
# Substitui todas as ocorrências de <strong>conteudo</strong>
text = re.sub(r'<strong>(.*?)</strong>', replace_strong_tags, text, flags=re.DOTALL)
# Substitui todas as ocorrências de <em>conteudo</em>
text = re.sub(r'<em>(.*?)</em>', replace_em_tags, text, flags=re.DOTALL)
max_length = 2200
suffix = '\n\n💬 Continua nos comentários!'
if len(text) <= max_length:
return text, None
cutoff_length = max_length - len(suffix)
if cutoff_length <= 0:
return suffix.strip(), text
trimmed = text[:cutoff_length]
def is_inside_quotes(s: str, index: int) -> bool:
"""Verifica se há aspas abertas não fechadas até o índice"""
up_to_index = s[:index + 1]
quote_count = up_to_index.count('"')
return quote_count % 2 != 0
# Encontra o último ponto final fora de aspas
last_valid_dot = -1
for i in range(len(trimmed) - 1, -1, -1):
if trimmed[i] == '.' and not is_inside_quotes(trimmed, i):
last_valid_dot = i
break
if last_valid_dot > 100:
main_text = trimmed[:last_valid_dot + 1]
remaining_text = text[last_valid_dot + 1:].strip()
else:
main_text = trimmed
remaining_text = text[cutoff_length:].strip()
final_main_text = f"{main_text}{suffix}"
return final_main_text, remaining_text if remaining_text else None
async def post_comment(client: httpx.AsyncClient, post_id: str, comment_text: str) -> Optional[str]:
"""
Posta um comentário no post do Instagram
Returns:
Optional[str]: ID do comentário se postado com sucesso
"""
try:
comment_url = f"{INSTAGRAM_API_BASE}/{post_id}/comments"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {INSTAGRAM_TOKEN}"
}
comment_payload = {
"message": comment_text
}
print(f"💬 Postando comentário no post {post_id}")
comment_response = await client.post(
comment_url,
headers=headers,
json=comment_payload
)
if comment_response.status_code == 200:
comment_data = comment_response.json()
comment_id = comment_data.get("id")
print(f"✅ Comentário postado com ID: {comment_id}")
return comment_id
else:
error_detail = comment_response.text
print(f"⚠️ Erro ao postar comentário: {error_detail}")
return None
except Exception as e:
print(f"⚠️ Erro inesperado ao postar comentário: {str(e)}")
return None
# 🚀 Endpoint principal para publicar no Instagram
@router.post("/publish", response_model=PublishResponse)
async def publish_instagram_post(post: InstagramPost) -> PublishResponse:
"""
Publica uma imagem no Instagram em duas etapas:
1. Cria o media container
2. Publica o post
3. Se necessário, posta o resto do texto como comentário
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
# 📝 Processa o texto da caption
main_caption, remaining_text = format_text_for_instagram(post.caption) if post.caption else ("", None)
# 🎯 ETAPA 1: Criar o media container
media_payload = {
"image_url": post.image_url
}
# Adiciona caption processada se fornecida
if main_caption:
media_payload["caption"] = main_caption
media_url = f"{INSTAGRAM_API_BASE}/{INSTAGRAM_PAGE_ID}/media"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {INSTAGRAM_TOKEN}"
}
print(f"📤 Criando media container para: {post.image_url}")
if remaining_text:
print(f"✂️ Texto cortado - será postado comentário com {len(remaining_text)} caracteres")
media_response = await client.post(
media_url,
headers=headers,
json=media_payload
)
if media_response.status_code != 200:
error_detail = media_response.text
print(f"❌ Erro ao criar media container: {error_detail}")
raise HTTPException(
status_code=media_response.status_code,
detail=f"Erro ao criar media container: {error_detail}"
)
media_data = media_response.json()
media_id = media_data.get("id")
if not media_id:
raise HTTPException(
status_code=500,
detail="ID do media container não retornado"
)
print(f"✅ Media container criado com ID: {media_id}")
# 🎯 ETAPA 2: Publicar o post
publish_payload = {
"creation_id": media_id
}
publish_url = f"{INSTAGRAM_API_BASE}/{INSTAGRAM_PAGE_ID}/media_publish"
print(f"📤 Publicando post com creation_id: {media_id}")
publish_response = await client.post(
publish_url,
headers=headers,
json=publish_payload
)
if publish_response.status_code != 200:
error_detail = publish_response.text
print(f"❌ Erro ao publicar post: {error_detail}")
raise HTTPException(
status_code=publish_response.status_code,
detail=f"Erro ao publicar post: {error_detail}"
)
publish_data = publish_response.json()
post_id = publish_data.get("id")
# 🔗 ETAPA 3: Obter detalhes do post para construir URL
post_url = None
if post_id:
try:
# Query para obter o permalink do post
post_details_url = f"{INSTAGRAM_API_BASE}/{post_id}?fields=permalink"
details_response = await client.get(post_details_url, headers=headers)
if details_response.status_code == 200:
details_data = details_response.json()
post_url = details_data.get("permalink")
print(f"🔗 Link do post: {post_url}")
else:
print(f"⚠️ Não foi possível obter o link do post: {details_response.text}")
except Exception as e:
print(f"⚠️ Erro ao obter link do post: {str(e)}")
# 💬 ETAPA 4: Postar comentário com o resto do texto (se necessário)
comment_posted = False
comment_id = None
if remaining_text and post_id:
comment_id = await post_comment(client, post_id, remaining_text)
comment_posted = comment_id is not None
success_message = "Post publicado com sucesso no Instagram!"
if comment_posted:
success_message += " Texto adicional postado como comentário."
elif remaining_text and not comment_posted:
success_message += " ATENÇÃO: Não foi possível postar o comentário com o resto do texto."
print(f"🎉 {success_message} Post ID: {post_id}")
return PublishResponse(
success=True,
media_id=media_id,
post_id=post_id,
post_url=post_url,
message=success_message,
comment_posted=comment_posted,
comment_id=comment_id
)
except httpx.TimeoutException:
print("⏰ Timeout na requisição para Instagram API")
raise HTTPException(
status_code=408,
detail="Timeout na comunicação com a API do Instagram"
)
except httpx.RequestError as e:
print(f"🌐 Erro de conexão: {str(e)}")
raise HTTPException(
status_code=502,
detail=f"Erro de conexão: {str(e)}"
)
except Exception as e:
print(f"💥 Erro inesperado: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Erro interno do servidor: {str(e)}"
) |