newapi-clone / routers /instagram.py
habulaj's picture
Upload 13 files
4ffe0a9 verified
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)}"
)