import os
import logging
import aiohttp
import pytz
import base64
import asyncio
from fastapi import APIRouter, HTTPException, Header, Body
from pydantic import BaseModel
from dateutil import parser
from datetime import datetime
from typing import Optional
logger = logging.getLogger("uvicorn.error")
router = APIRouter()
# 🔧 Supabase Config
SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co"
SUPABASE_KEY = os.getenv("SUPA_KEY")
SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY")
if not SUPABASE_KEY or not SUPABASE_ROLE_KEY:
raise ValueError("❌ SUPA_KEY or SUPA_SERVICE_KEY not set in environment!")
SUPABASE_HEADERS = {
"apikey": SUPABASE_KEY,
"Authorization": f"Bearer {SUPABASE_KEY}",
"Content-Type": "application/json"
}
SUPABASE_ROLE_HEADERS = {
"apikey": SUPABASE_ROLE_KEY,
"Authorization": f"Bearer {SUPABASE_ROLE_KEY}",
"Content-Type": "application/json",
"Prefer": "return=representation"
}
# 🛡️ Verificação de token de usuário (sem admin check)
async def verify_user_token(user_token: str) -> str:
headers = {
"Authorization": f"Bearer {user_token}",
"apikey": SUPABASE_KEY,
"Content-Type": "application/json"
}
async with aiohttp.ClientSession() as session:
async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response:
if response.status != 200:
raise HTTPException(status_code=401, detail="Token inválido ou expirado")
user_data = await response.json()
user_id = user_data.get("id")
if not user_id:
raise HTTPException(status_code=400, detail="ID do usuário não encontrado")
return user_id
# 📨 Modelo da requisição de ticket
class CreateTicketRequest(BaseModel):
message: str
subject: str
priority: str
def format_datetime(dt_str):
dt = parser.isoparse(dt_str)
return dt.strftime("%m/%d/%Y, %I:%M %p") # Hora com AM/PM
@router.get("/ticket/user")
async def get_user_tickets(
user_token: str = Header(None, alias="User-key")
):
user_id = await verify_user_token(user_token)
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?user_id=eq.{user_id}&order=created_at.desc&limit=50",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
error_detail = await ticket_resp.text()
raise HTTPException(status_code=500, detail=f"Error fetching tickets: {error_detail}")
tickets = await ticket_resp.json()
ticket_results = []
for ticket in tickets:
ticket_id = ticket["id"]
ticket_created = parser.isoparse(ticket["created_at"])
formatted_ticket_date = ticket_created.strftime("%m/%d/%Y")
# Buscar a primeira mensagem
async with session.get(
f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.asc&limit=1",
headers=SUPABASE_ROLE_HEADERS
) as first_msg_resp:
first_message = None
if first_msg_resp.status == 200:
messages = await first_msg_resp.json()
first_message = messages[0] if messages else None
# Buscar todas mensagens para histórico e status
async with session.get(
f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.asc",
headers=SUPABASE_ROLE_HEADERS
) as all_msg_resp:
all_messages = await all_msg_resp.json() if all_msg_resp.status == 200 else []
history = [f"Ticket created on {ticket_created.strftime('%m/%d/%Y, %I:%M %p')}"]
# Detectar mudanças de suporte
assigned_support_ids = []
last_support_id = None
for msg in all_messages:
msg_user = msg["user"]
if msg_user != user_id and msg_user != last_support_id:
last_support_id = msg_user
assigned_support_ids.append((msg_user, msg["created_at"]))
for support_id, assigned_time in assigned_support_ids:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{support_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
support_name = "Support"
if user_resp.status == 200:
user_data = await user_resp.json()
if user_data:
support_name = user_data[0].get("name", "Support")
history.append(f"Assigned to {support_name} on {format_datetime(assigned_time)}")
# Status e última atualização (se necessário)
status = "Open"
if ticket.get("finished"):
status = "Closed"
if ticket.get("finished_date"):
closed_dt = parser.isoparse(ticket["finished_date"])
history.append(f"Ticket closed on {closed_dt.strftime('%m/%d/%Y, %I:%M %p')}")
elif all_messages:
last_msg = all_messages[-1]
if last_msg["user"] != user_id:
status = "Answered"
else:
status = "Open" # já está por padrão, mas pode ser útil explicitar
if len(all_messages) > 1:
# Sempre adicionar o last updated se houver mais de uma mensagem
last_msg_time = format_datetime(last_msg["created_at"])
history.append(f"Last updated on {last_msg_time}")
# Montar retorno do ticket
ticket_data = dict(ticket)
ticket_data["formatted_date"] = formatted_ticket_date
ticket_data["status"] = status
if ticket.get("support_id"):
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{ticket['support_id']}",
headers=SUPABASE_ROLE_HEADERS
) as support_resp:
if support_resp.status == 200:
support_data = await support_resp.json()
if support_data:
support_info = support_data[0]
ticket_data["support_name"] = support_info.get("name")
ticket_data["support_email"] = support_info.get("email")
ticket_results.append({
"ticket": ticket_data,
"first_message": first_message,
"history": history
})
return ticket_results
@router.get("/ticket/detail")
async def get_ticket_details(ticket_id: int):
async with aiohttp.ClientSession() as session:
# 1. Buscar dados do ticket
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket não encontrado")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket inexistente")
ticket = ticket_data[0]
# 2. Buscar as 50 últimas mensagens
async with session.get(
f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.desc&limit=50",
headers=SUPABASE_ROLE_HEADERS
) as msg_resp:
if msg_resp.status != 200:
raise HTTPException(status_code=500, detail="Erro ao buscar mensagens")
messages_raw = await msg_resp.json()
# 3. Buscar info dos usuários das mensagens
user_cache = {}
for msg in messages_raw:
user_id = msg["user"]
if user_id not in user_cache:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
if user_resp.status == 200:
user_data = await user_resp.json()
if user_data:
u = user_data[0]
user_cache[user_id] = {
"id": u["id"],
"name": u.get("name", "Desconhecido"),
"avatar": u.get("avatar", None),
"type": "support" if u.get("role") == "admin" else "customer"
}
else:
user_cache[user_id] = {
"id": user_id,
"name": "Desconhecido",
"avatar": None,
"type": "unknown"
}
# 4. Substituir o campo user nas mensagens
messages = []
for msg in messages_raw:
uid = msg["user"]
user_info = user_cache.get(uid, {
"id": uid,
"name": "Desconhecido",
"avatar": None,
})
# Definir o tipo com base no ticket
if uid == ticket["user_id"]:
user_info["type"] = "customer"
else:
user_info["type"] = "support"
messages.append({
**msg,
"user": user_info
})
return {
"ticket": ticket,
"messages": messages
}
class RespondTicketRequest(BaseModel):
ticket_id: int
content: str
@router.post("/ticket/respond")
async def respond_ticket(
body: RespondTicketRequest,
user_token: str = Header(None, alias="User-key")
):
user_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
# 1. Buscar dados do ticket
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{body.ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket não encontrado")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket inexistente")
ticket = ticket_data[0]
support_id = ticket.get("support_id")
ticket_creator_id = ticket.get("user_id") # <- CORRIGIDO AQUI
# 2. Verificar/Atribuir suporte
if support_id is None:
# Verifica se usuário é admin
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
if user_resp.status != 200:
raise HTTPException(status_code=403, detail="Usuário inválido")
user_data = await user_resp.json()
if not user_data or not user_data[0].get("is_admin", False):
raise HTTPException(status_code=403, detail="Apenas administradores podem assumir tickets")
# Atualiza support_id
async with aiohttp.ClientSession() as session:
async with session.patch(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{body.ticket_id}",
headers={**SUPABASE_ROLE_HEADERS, "Prefer": "return=representation"},
json={"support_id": user_id}
) as patch_resp:
if patch_resp.status != 200:
raise HTTPException(status_code=500, detail="Erro ao atribuir o suporte ao ticket")
else:
# Permitir exceção se o usuário for o criador do ticket
if support_id != user_id and user_id != ticket_creator_id:
raise HTTPException(status_code=403, detail="Ticket já está atribuído a outro suporte")
# 3. Espera 1 segundo antes de enviar a mensagem
await asyncio.sleep(1)
# 3.1. Reabrir ticket se estiver fechado
if ticket.get("finished", False) is True:
async with aiohttp.ClientSession() as session:
async with session.patch(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{body.ticket_id}",
headers={**SUPABASE_ROLE_HEADERS, "Prefer": "return=representation"},
json={"finished": False, "finished_date": None}
) as reopen_resp:
if reopen_resp.status != 200:
raise HTTPException(status_code=500, detail="Erro ao reabrir o ticket")
# 4. Criar mensagem
message_payload = {
"user": user_id,
"content": body.content,
"created_at": created_at,
"ticket_id": body.ticket_id
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as message_resp:
if message_resp.status != 201:
error_detail = await message_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao salvar a mensagem: {error_detail}")
message_data = await message_resp.json()
return {
"status": "response sent successfully",
"ticket_id": body.ticket_id,
"message_id": message_data[0]["id"],
"message_content": body.content
}
@router.post("/ticket/create")
async def create_ticket(
body: CreateTicketRequest,
user_token: str = Header(None, alias="User-key")
):
user_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
ticket_payload = {
"user_id": user_id,
"support_id": None,
"created_at": created_at,
"subject": body.subject,
"priority": body.priority
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/Tickets",
headers=SUPABASE_ROLE_HEADERS,
json=ticket_payload
) as ticket_resp:
if ticket_resp.status != 201:
error_detail = await ticket_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao criar ticket: {error_detail}")
ticket_data = await ticket_resp.json()
ticket_id = ticket_data[0]["id"]
message_payload = {
"user": user_id,
"content": body.message,
"created_at": created_at,
"ticket_id": ticket_id
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as message_resp:
if message_resp.status != 201:
error_detail = await message_resp.text()
raise HTTPException(status_code=500, detail=f"Erro ao criar mensagem: {error_detail}")
return {"ticket_id": ticket_id}
class CloseTicketRequest(BaseModel):
ticket_id: int
@router.post("/ticket/close")
async def close_ticket(
body: CloseTicketRequest,
user_token: str = Header(None, alias="User-key")
):
user_id = await verify_user_token(user_token)
logger.info(f"User ID verified: {user_id}")
finished_date = datetime.utcnow().isoformat()
# Verificar se o ticket já está finalizado
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{body.ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as get_resp:
if get_resp.status != 200:
error_detail = await get_resp.text()
logger.error(f"Error fetching ticket: {error_detail}")
raise HTTPException(status_code=500, detail=f"Error fetching ticket: {error_detail}")
ticket_data = await get_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket not found")
ticket = ticket_data[0]
if ticket.get("finished", False):
logger.info(f"Ticket {body.ticket_id} is already closed.")
return {"message": "Ticket is already closed", "ticket_id": body.ticket_id}
# Atualizar o finished e finished_date
update_payload = {
"finished": True,
"finished_date": finished_date
}
logger.info(f"Updating ticket with payload: {update_payload}")
try:
async with aiohttp.ClientSession() as session:
async with session.patch(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{body.ticket_id}",
headers={
**SUPABASE_ROLE_HEADERS,
"Content-Type": "application/json"
},
json=update_payload
) as update_resp:
logger.info(f"Response status: {update_resp.status}")
response_data = await update_resp.json() # Alterado de text() para json(), pois é uma resposta JSON.
logger.info(f"Response data: {response_data}")
if update_resp.status == 200:
# Verificar se o ticket foi realmente atualizado corretamente.
updated_ticket = response_data[0]
if updated_ticket["finished"] and updated_ticket["finished_date"]:
logger.info(f"Ticket {body.ticket_id} closed successfully.")
return {"message": "Ticket closed successfully", "ticket_id": body.ticket_id}
else:
logger.error(f"Ticket update failed: {updated_ticket}")
raise HTTPException(status_code=500, detail="Ticket update failed.")
else:
logger.error(f"Error updating ticket: {response_data}")
raise HTTPException(status_code=500, detail=f"Error updating ticket: {response_data}")
except aiohttp.ClientError as e:
logger.error(f"AIOHTTP client error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Client error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
# 📧 Envio de e-mails com Gmail API
GMAIL_CLIENT_ID = "784687789817-3genmmvps11ip3a6fkbkkd8dm3bstgdc.apps.googleusercontent.com"
GMAIL_CLIENT_SECRET = "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"
GMAIL_REFRESH_TOKEN = "1//04ZOO_chVwlYiCgYIARAAGAQSNwF-L9IrhQO1ij79thk-DTjiMudl_XQshuU5CDTDYtt8rrOTMbz_rL8ECGjNfEN9da6W-mnjhZA"
class TicketResponseRequest(BaseModel):
ticket_id: int
content: str
async def get_gmail_access_token() -> str:
url = "https://oauth2.googleapis.com/token"
data = {
"client_id": GMAIL_CLIENT_ID,
"client_secret": GMAIL_CLIENT_SECRET,
"refresh_token": GMAIL_REFRESH_TOKEN,
"grant_type": "refresh_token"
}
async with aiohttp.ClientSession() as session:
async with session.post(url, data=data) as response:
if response.status != 200:
raise HTTPException(status_code=500, detail="Erro ao obter access_token do Gmail")
token_data = await response.json()
return token_data["access_token"]
def encode_message(raw_message: str) -> str:
return base64.urlsafe_b64encode(raw_message.encode("utf-8")).decode("utf-8").replace("=", "")
@router.post("/ticket/support/respond")
async def respond_ticket(
payload: TicketResponseRequest,
user_token: str = Header(None, alias="User-key")
):
# 1. Verify support agent token
support_id = await verify_user_token(user_token)
created_at = datetime.utcnow().isoformat()
# Get support agent name
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{support_id}",
headers=SUPABASE_ROLE_HEADERS
) as support_user_resp:
if support_user_resp.status != 200:
raise HTTPException(status_code=404, detail="Support agent not found")
support_user_data = await support_user_resp.json()
if not support_user_data:
raise HTTPException(status_code=404, detail="Support agent does not exist")
support_name = support_user_data[0].get("name", "Support Team").strip()
# 2. Retrieve ticket
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{payload.ticket_id}",
headers=SUPABASE_ROLE_HEADERS
) as ticket_resp:
if ticket_resp.status != 200:
raise HTTPException(status_code=404, detail="Ticket not found")
ticket_data = await ticket_resp.json()
if not ticket_data:
raise HTTPException(status_code=404, detail="Ticket does not exist")
ticket = ticket_data[0]
user_id = ticket["user_id"]
# 3. Get user email and name
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}",
headers=SUPABASE_ROLE_HEADERS
) as user_resp:
if user_resp.status != 200:
raise HTTPException(status_code=404, detail="User not found")
user_data = await user_resp.json()
if not user_data:
raise HTTPException(status_code=404, detail="User does not exist")
user_email = user_data[0]["email"]
user_name = user_data[0].get("name", "user").strip()
first_name = user_name.split(" ")[0] if user_name else "user"
# 4. Send email with clean, elegant HTML template with brand purple gradient
access_token = await get_gmail_access_token()
subject = f"Customer Support - Case {payload.ticket_id}"
email_html = f"""
|
Hello {first_name},
{payload.content}
Best regards,
{support_name}
Customer Support
|
|
ClosetCoach © 2025
|
|
"""
raw_message = f"""To: {user_email}
From: "ClosetCoach"
Subject: {subject}
Content-Type: text/html; charset="UTF-8"
{email_html}
"""
encoded_message = encode_message(raw_message)
async with aiohttp.ClientSession() as session:
async with session.post(
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
json={"raw": encoded_message}
) as gmail_resp:
if gmail_resp.status != 200:
error_detail = await gmail_resp.text()
raise HTTPException(status_code=500, detail=f"Error sending email: {error_detail}")
gmail_data = await gmail_resp.json()
# 5. Save response in messages_tickets
message_payload = {
"user": support_id,
"content": payload.content,
"created_at": created_at,
"ticket_id": payload.ticket_id
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SUPABASE_URL}/rest/v1/messages_tickets",
headers=SUPABASE_ROLE_HEADERS,
json=message_payload
) as msg_resp:
if msg_resp.status != 201:
error_detail = await msg_resp.text()
raise HTTPException(status_code=500, detail=f"Error recording response: {error_detail}")
return {
"status": "response sent successfully",
"ticket_id": payload.ticket_id,
"email_to": user_email,
"message_id": gmail_data.get("id")
}