habulaj commited on
Commit
0934632
·
verified ·
1 Parent(s): 882fdf1

Update routers/instagram.py

Browse files
Files changed (1) hide show
  1. routers/instagram.py +270 -80
routers/instagram.py CHANGED
@@ -1,104 +1,294 @@
1
  import os
2
- import requests
 
3
  from fastapi import APIRouter, HTTPException
4
  from pydantic import BaseModel
5
- import socket
6
 
7
  router = APIRouter()
8
 
9
- # 📌 Instagram Graph API
10
- INSTAGRAM_ACCESS_TOKEN = os.getenv("INSTAGRAM_ACCESS_TOKEN")
11
- INSTAGRAM_USER_ID = "17841464166934843"
 
12
 
13
- if not INSTAGRAM_ACCESS_TOKEN:
14
- raise ValueError("❌ INSTAGRAM_ACCESS_TOKEN não configurado no ambiente!")
15
 
16
- # 🔧 Configura DNS programaticamente
17
- def setup_dns():
18
- """Configura DNS alternativo se necessário"""
19
- try:
20
- # Testa se consegue resolver
21
- socket.gethostbyname('google.com')
22
- return True
23
- except:
24
- # Se falhar, configura DNS manual
25
- import dns.resolver
26
- dns.resolver.default_resolver = dns.resolver.Resolver(configure=False)
27
- dns.resolver.default_resolver.nameservers = ['8.8.8.8', '8.8.4.4']
28
- return False
29
 
30
- # Executa setup DNS na importação
31
- try:
32
- setup_dns()
33
- except:
34
- pass
 
 
 
35
 
36
- class InstagramMedia(BaseModel):
37
- image_url: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- @router.post("/instagram/container")
40
- async def create_instagram_container(media: InstagramMedia):
41
- url = f"https://graph.instagram.com/v23.0/{INSTAGRAM_USER_ID}/media"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- headers = {
44
- "Content-Type": "application/json",
45
- "Authorization": f"Bearer {INSTAGRAM_ACCESS_TOKEN}"
46
- }
47
 
48
- payload = {"image_url": media.image_url}
 
 
49
 
50
- # Configurar sessão com configurações específicas para resolver DNS
51
- session = requests.Session()
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  try:
54
- # Força uso de DNS específico via requests
55
- response = session.post(
56
- url,
57
- headers=headers,
58
- json=payload,
59
- timeout=30.0,
60
- # Configurações extras para problemas de rede
61
- allow_redirects=True,
62
- verify=True
63
- )
64
 
65
- if response.status_code != 200:
66
- raise HTTPException(status_code=response.status_code, detail=response.text)
 
67
 
68
- data = response.json()
69
- return {"id": data.get("id")}
 
 
 
 
70
 
71
- except requests.exceptions.ConnectionError as e:
72
- if "name resolution" in str(e).lower():
73
- # Tenta com IP direto como fallback
74
- try:
75
- ip_url = url.replace("graph.instagram.com", "31.13.24.51")
76
- headers["Host"] = "graph.instagram.com"
77
-
78
- response = session.post(
79
- ip_url,
80
- headers=headers,
81
- json=payload,
82
- timeout=30.0,
83
- verify=False # Necessário quando usando IP
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  )
85
-
86
- if response.status_code != 200:
87
- raise HTTPException(status_code=response.status_code, detail=response.text)
88
-
89
- data = response.json()
90
- return {"id": data.get("id")}
91
-
92
- except Exception as fallback_error:
93
  raise HTTPException(
94
  status_code=500,
95
- detail=f"DNS error and IP fallback failed: {fallback_error}"
96
  )
97
- else:
98
- raise HTTPException(status_code=500, detail=f"Connection error: {e}")
99
 
100
- except Exception as e:
101
- raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")
102
-
103
- finally:
104
- session.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import httpx
3
+ from typing import Dict, Optional, Tuple
4
  from fastapi import APIRouter, HTTPException
5
  from pydantic import BaseModel
 
6
 
7
  router = APIRouter()
8
 
9
+ # 📱 Instagram API Config
10
+ INSTAGRAM_API_BASE = "https://graph.instagram.com/v23.0"
11
+ INSTAGRAM_PAGE_ID = "17841464166934843" # Seu Page ID
12
+ INSTAGRAM_TOKEN = os.getenv("INSTAGRAM_ACCESS_TOKEN")
13
 
14
+ if not INSTAGRAM_TOKEN:
15
+ raise ValueError("❌ INSTAGRAM_ACCESS_TOKEN não foi definido nas variáveis de ambiente!")
16
 
17
+ # 📝 Modelos de dados
18
+ class InstagramPost(BaseModel):
19
+ image_url: str
20
+ caption: Optional[str] = None
 
 
 
 
 
 
 
 
 
21
 
22
+ class PublishResponse(BaseModel):
23
+ success: bool
24
+ media_id: Optional[str] = None
25
+ post_id: Optional[str] = None
26
+ post_url: Optional[str] = None
27
+ message: str
28
+ comment_posted: bool = False
29
+ comment_id: Optional[str] = None
30
 
31
+ def clean_html_tags(text: str) -> str:
32
+ """
33
+ Remove todas as tags HTML.
34
+ Converte <p> para quebras de linha.
35
+
36
+ Returns:
37
+ str: Texto limpo sem tags HTML
38
+ """
39
+ if not text:
40
+ return ""
41
+
42
+ import re
43
+
44
+ # Converte <p> para quebras de linha
45
+ text = re.sub(r'<p[^>]*>(.*?)</p>', r'\1\n\n', text, flags=re.DOTALL | re.IGNORECASE)
46
+
47
+ # Converte <br> e <br/> para quebra de linha simples
48
+ text = re.sub(r'<br\s*/?>', '\n', text, flags=re.IGNORECASE)
49
+
50
+ # Remove todas as tags HTML
51
+ text = re.sub(r'<[^>]*>', '', text, flags=re.IGNORECASE)
52
+
53
+ # Remove quebras de linha excessivas e espaços extras
54
+ text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text) # Máximo de 2 quebras consecutivas
55
+ text = re.sub(r'[ \t]+', ' ', text) # Remove espaços/tabs extras
56
+ text = text.strip()
57
+
58
+ return text
59
 
60
+ def format_text_for_instagram(text: str) -> Tuple[str, Optional[str]]:
61
+ """
62
+ Formata o texto para o Instagram:
63
+ 1. Limpa tags HTML
64
+ 2. Corta se necessário e retorna o texto principal e o resto para comentário
65
+
66
+ Returns:
67
+ Tuple[str, Optional[str]]: (texto_principal, resto_para_comentario)
68
+ """
69
+ if not text:
70
+ return "", None
71
+
72
+ # 🧹 Limpa as tags HTML
73
+ text = clean_html_tags(text)
74
+
75
+ max_length = 2200
76
+ suffix = '\n\n💬 Continua nos comentários!'
77
 
78
+ if len(text) <= max_length:
79
+ return text, None
 
 
80
 
81
+ cutoff_length = max_length - len(suffix)
82
+ if cutoff_length <= 0:
83
+ return suffix.strip(), text
84
 
85
+ trimmed = text[:cutoff_length]
 
86
 
87
+ def is_inside_quotes(s: str, index: int) -> bool:
88
+ """Verifica se há aspas abertas não fechadas até o índice"""
89
+ up_to_index = s[:index + 1]
90
+ quote_count = up_to_index.count('"')
91
+ return quote_count % 2 != 0
92
+
93
+ # Encontra o último ponto final fora de aspas
94
+ last_valid_dot = -1
95
+ for i in range(len(trimmed) - 1, -1, -1):
96
+ if trimmed[i] == '.' and not is_inside_quotes(trimmed, i):
97
+ last_valid_dot = i
98
+ break
99
+
100
+ if last_valid_dot > 100:
101
+ main_text = trimmed[:last_valid_dot + 1]
102
+ remaining_text = text[last_valid_dot + 1:].strip()
103
+ else:
104
+ main_text = trimmed
105
+ remaining_text = text[cutoff_length:].strip()
106
+
107
+ final_main_text = f"{main_text}{suffix}"
108
+
109
+ return final_main_text, remaining_text if remaining_text else None
110
+
111
+ async def post_comment(client: httpx.AsyncClient, post_id: str, comment_text: str) -> Optional[str]:
112
+ """
113
+ Posta um comentário no post do Instagram
114
+
115
+ Returns:
116
+ Optional[str]: ID do comentário se postado com sucesso
117
+ """
118
  try:
119
+ comment_url = f"{INSTAGRAM_API_BASE}/{post_id}/comments"
120
+ headers = {
121
+ "Content-Type": "application/json",
122
+ "Authorization": f"Bearer {INSTAGRAM_TOKEN}"
123
+ }
 
 
 
 
 
124
 
125
+ comment_payload = {
126
+ "message": comment_text
127
+ }
128
 
129
+ print(f"💬 Postando comentário no post {post_id}")
130
+ comment_response = await client.post(
131
+ comment_url,
132
+ headers=headers,
133
+ json=comment_payload
134
+ )
135
 
136
+ if comment_response.status_code == 200:
137
+ comment_data = comment_response.json()
138
+ comment_id = comment_data.get("id")
139
+ print(f"✅ Comentário postado com ID: {comment_id}")
140
+ return comment_id
141
+ else:
142
+ error_detail = comment_response.text
143
+ print(f"⚠️ Erro ao postar comentário: {error_detail}")
144
+ return None
145
+
146
+ except Exception as e:
147
+ print(f"⚠️ Erro inesperado ao postar comentário: {str(e)}")
148
+ return None
149
+
150
+ # 🚀 Endpoint principal para publicar no Instagram
151
+ @router.post("/publish", response_model=PublishResponse)
152
+ async def publish_instagram_post(post: InstagramPost) -> PublishResponse:
153
+ """
154
+ Publica uma imagem no Instagram em duas etapas:
155
+ 1. Cria o media container
156
+ 2. Publica o post
157
+ 3. Se necessário, posta o resto do texto como comentário
158
+ """
159
+
160
+ async with httpx.AsyncClient(timeout=30.0) as client:
161
+ try:
162
+ # 📝 Processa o texto da caption
163
+ main_caption, remaining_text = format_text_for_instagram(post.caption) if post.caption else ("", None)
164
+
165
+ # 🎯 ETAPA 1: Criar o media container
166
+ media_payload = {
167
+ "image_url": post.image_url
168
+ }
169
+
170
+ # Adiciona caption processada se fornecida
171
+ if main_caption:
172
+ media_payload["caption"] = main_caption
173
+
174
+ media_url = f"{INSTAGRAM_API_BASE}/{INSTAGRAM_PAGE_ID}/media"
175
+ headers = {
176
+ "Content-Type": "application/json",
177
+ "Authorization": f"Bearer {INSTAGRAM_TOKEN}"
178
+ }
179
+
180
+ print(f"📤 Criando media container para: {post.image_url}")
181
+ if remaining_text:
182
+ print(f"✂️ Texto cortado - será postado comentário com {len(remaining_text)} caracteres")
183
+
184
+ media_response = await client.post(
185
+ media_url,
186
+ headers=headers,
187
+ json=media_payload
188
+ )
189
+
190
+ if media_response.status_code != 200:
191
+ error_detail = media_response.text
192
+ print(f"❌ Erro ao criar media container: {error_detail}")
193
+ raise HTTPException(
194
+ status_code=media_response.status_code,
195
+ detail=f"Erro ao criar media container: {error_detail}"
196
  )
197
+
198
+ media_data = media_response.json()
199
+ media_id = media_data.get("id")
200
+
201
+ if not media_id:
 
 
 
202
  raise HTTPException(
203
  status_code=500,
204
+ detail="ID do media container não retornado"
205
  )
 
 
206
 
207
+ print(f"✅ Media container criado com ID: {media_id}")
208
+
209
+ # 🎯 ETAPA 2: Publicar o post
210
+ publish_payload = {
211
+ "creation_id": media_id
212
+ }
213
+
214
+ publish_url = f"{INSTAGRAM_API_BASE}/{INSTAGRAM_PAGE_ID}/media_publish"
215
+
216
+ print(f"📤 Publicando post com creation_id: {media_id}")
217
+ publish_response = await client.post(
218
+ publish_url,
219
+ headers=headers,
220
+ json=publish_payload
221
+ )
222
+
223
+ if publish_response.status_code != 200:
224
+ error_detail = publish_response.text
225
+ print(f"❌ Erro ao publicar post: {error_detail}")
226
+ raise HTTPException(
227
+ status_code=publish_response.status_code,
228
+ detail=f"Erro ao publicar post: {error_detail}"
229
+ )
230
+
231
+ publish_data = publish_response.json()
232
+ post_id = publish_data.get("id")
233
+
234
+ # 🔗 ETAPA 3: Obter detalhes do post para construir URL
235
+ post_url = None
236
+ if post_id:
237
+ try:
238
+ # Query para obter o permalink do post
239
+ post_details_url = f"{INSTAGRAM_API_BASE}/{post_id}?fields=permalink"
240
+ details_response = await client.get(post_details_url, headers=headers)
241
+
242
+ if details_response.status_code == 200:
243
+ details_data = details_response.json()
244
+ post_url = details_data.get("permalink")
245
+ print(f"🔗 Link do post: {post_url}")
246
+ else:
247
+ print(f"⚠️ Não foi possível obter o link do post: {details_response.text}")
248
+ except Exception as e:
249
+ print(f"⚠️ Erro ao obter link do post: {str(e)}")
250
+
251
+ # 💬 ETAPA 4: Postar comentário com o resto do texto (se necessário)
252
+ comment_posted = False
253
+ comment_id = None
254
+
255
+ if remaining_text and post_id:
256
+ comment_id = await post_comment(client, post_id, remaining_text)
257
+ comment_posted = comment_id is not None
258
+
259
+ success_message = "Post publicado com sucesso no Instagram!"
260
+ if comment_posted:
261
+ success_message += " Texto adicional postado como comentário."
262
+ elif remaining_text and not comment_posted:
263
+ success_message += " ATENÇÃO: Não foi possível postar o comentário com o resto do texto."
264
+
265
+ print(f"🎉 {success_message} Post ID: {post_id}")
266
+
267
+ return PublishResponse(
268
+ success=True,
269
+ media_id=media_id,
270
+ post_id=post_id,
271
+ post_url=post_url,
272
+ message=success_message,
273
+ comment_posted=comment_posted,
274
+ comment_id=comment_id
275
+ )
276
+
277
+ except httpx.TimeoutException:
278
+ print("⏰ Timeout na requisição para Instagram API")
279
+ raise HTTPException(
280
+ status_code=408,
281
+ detail="Timeout na comunicação com a API do Instagram"
282
+ )
283
+ except httpx.RequestError as e:
284
+ print(f"🌐 Erro de conexão: {str(e)}")
285
+ raise HTTPException(
286
+ status_code=502,
287
+ detail=f"Erro de conexão: {str(e)}"
288
+ )
289
+ except Exception as e:
290
+ print(f"💥 Erro inesperado: {str(e)}")
291
+ raise HTTPException(
292
+ status_code=500,
293
+ detail=f"Erro interno do servidor: {str(e)}"
294
+ )