brunorreiss commited on
Commit
af5bbee
·
verified ·
1 Parent(s): 1946c63

Upload 33 files

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__
2
+ .venv
3
+ .vscode
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-buster
2
+
3
+
4
+ # Atualiza o sistema e instala dependências essenciais (exemplo: gcc para compilações nativas)
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ gcc \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Define o diretório de trabalho
10
+ WORKDIR /app
11
+
12
+ # Copia o arquivo de requisitos e instala as dependências Python
13
+ COPY requirements.txt .
14
+ RUN pip install --upgrade pip && pip install -r requirements.txt
15
+
16
+ # Copia o restante do código da aplicação para a imagem
17
+ COPY . .
18
+
19
+ # Exponha a porta desejada (neste exemplo, 80)
20
+ EXPOSE 80
21
+
22
+ # Comando para iniciar a aplicação usando uvicorn
23
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "80"]
Dockerrun.aws.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "AWSEBDockerrunVersion": "1",
3
+ "Image": {
4
+ "Name": "python:3.11-slim-buster",
5
+ "Update": "true"
6
+ },
7
+ "Ports": [
8
+ {
9
+ "ContainerPort": 5000,
10
+ "HostPort": 80
11
+ }
12
+ ],
13
+ "Logging": "/var/log"
14
+ }
README.md CHANGED
@@ -1,10 +1,43 @@
1
- ---
2
- title: Jurisbot
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Monitoramento de processos dos Tribunais de Justiça do Brasil. :robot:
2
+
3
+ ## Introdução
4
+ Monitoramento de processos dos Tribunais de Justiça do Brasil - BETA
5
+
6
+
7
+ ## Respostas da API
8
+
9
+ | HTTP CODE | `code` | Descrição |
10
+ | --------- | ---------- | --------- |
11
+ | `200` | 0 | Sucesso |
12
+ | `422` | 7 | Não foi possível processar |
13
+ | `502` | 5 | Bad Gateway |
14
+ | `512` | 4 | Erro ao executar parse da pagina |
15
+
16
+
17
+
18
+ ### Para executar o bot manualmente, siga os passos abaixo:
19
+
20
+ 1. Baixe o repositorio:
21
+ ```
22
+ git clone https://github.com/reissbruno/monitoramento-processual
23
+ ```
24
+ 2. Mude de diretorio:
25
+ ```
26
+ cd monitoramento-processual
27
+ ```
28
+ 3. Instale as bibliotecas requeridas:
29
+ ```
30
+ pip install -r requirements.txt
31
+ ```
32
+ 4. Execute o bot:
33
+ ```
34
+ uvicorn server:app --host 0.0.0.0
35
+ ```
36
+ 5. Abra o navegador a acesse a URL: http://localhost:8000/
37
+
38
+
39
+ | ENV VAR | Descrição |
40
+ | ------- | ---------- |
41
+ | `BOT_NAME` | Nome do bot. Util caso houver mais de um container do mesmo bot rodando. (*default: transparencia-receita-fortaleza*) |
42
+ | `LOG_LEVEL` | Nivel de log. Valores aceitos: (DEBUG, INFO, WARNING, ERROR - *default: INFO*) |
43
+ | `TIMEOUT` | Total de **segundos** que o bot espera antes de encerrar a conexao com o site. (*default: 180*) |
captcha_temporario.png ADDED
data_delete/deletar_advogado.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manipulação do bando de dados de adição de nomes de advogados, deletar do banco de dados
2
+
3
+ import sqlite3
4
+
5
+ DATABASE_FILE = "monitoramento.db"
6
+
7
+ def initialize_db():
8
+ conn = sqlite3.connect(DATABASE_FILE)
9
+ cursor = conn.cursor()
10
+ cursor.execute('''
11
+ CREATE TABLE IF NOT EXISTS monitor_advogados (
12
+ nome_advogado TEXT PRIMARY KEY,
13
+ credencial TEXT NOT NULL,
14
+ uf TEXT NOT NULL
15
+ )
16
+ ''')
17
+ conn.commit()
18
+ conn.close()
19
+
20
+ initialize_db()
21
+
22
+ async def delete_advogado(nome_advogado: str, uf: str, credencial: str):
23
+ """
24
+ Deleta um nome de advogado do banco de dados de monitoramento após verificar a credencial.
25
+
26
+ Args:
27
+ nome_advogado (str): Nome do advogado a ser removido.
28
+ uf (str): Unidade federativa relacionada.
29
+ credencial (str): Credencial do responsável pela requisição.
30
+
31
+ Returns:
32
+ dict: Mensagem com o status da operação.
33
+ """
34
+ try:
35
+ conn = sqlite3.connect(DATABASE_FILE)
36
+ cursor = conn.cursor()
37
+ # Buscar registro pelo nome do advogado e uf
38
+ cursor.execute('''
39
+ SELECT credencial FROM monitor_advogados WHERE nome_advogado = ? AND uf = ?
40
+ ''', (nome_advogado, uf))
41
+ row = cursor.fetchone()
42
+ if not row:
43
+ conn.close()
44
+ return {"code": 404, "message": "Advogado não encontrado."}
45
+ stored_credencial = row[0]
46
+ if stored_credencial != credencial:
47
+ conn.close()
48
+ return {"code": 403, "message": "Credencial inválida. Não autorizado para deletar este advogado."}
49
+ # Credencial verificada; realiza a deleção
50
+ cursor.execute('''
51
+ DELETE FROM monitor_advogados WHERE nome_advogado = ? AND uf = ?
52
+ ''', (nome_advogado, uf))
53
+ conn.commit()
54
+ conn.close()
55
+ return {"code": 200, "message": "Advogado deletado com sucesso."}
56
+ except Exception as e:
57
+ return {"code": 500, "message": f"Erro ao deletar advogado: {str(e)}"}
data_delete/deletar_processo.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manipulação do bando de dados de adição de processos, deletar do banco de dados
2
+
3
+ import sqlite3
4
+
5
+ DATABASE_FILE = "monitoramento.db"
6
+
7
+ def initialize_db():
8
+ conn = sqlite3.connect(DATABASE_FILE)
9
+ cursor = conn.cursor()
10
+ cursor.execute('''
11
+ CREATE TABLE IF NOT EXISTS monitor_processos (
12
+ numero_processo TEXT PRIMARY KEY,
13
+ credencial TEXT NOT NULL,
14
+ uf TEXT NOT NULL
15
+ )
16
+ ''')
17
+ conn.commit()
18
+ conn.close()
19
+
20
+ initialize_db()
21
+
22
+ async def delete_processo(numero_processo: str, uf: str, credencial: str):
23
+ """
24
+ Deleta um número de processo do banco de dados de monitoramento após verificar a credencial.
25
+
26
+ Args:
27
+ numero_processo (str): Número do processo a ser removido.
28
+ uf (str): Unidade federativa relacionada.
29
+ credencial (str): Credencial do responsável pela requisição.
30
+
31
+ Returns:
32
+ dict: Mensagem com o status da operação.
33
+ """
34
+ try:
35
+ conn = sqlite3.connect(DATABASE_FILE)
36
+ cursor = conn.cursor()
37
+ # Buscar registro pelo número do processo e uf
38
+ cursor.execute('''
39
+ SELECT credencial FROM monitor_processos WHERE numero_processo = ? AND uf = ?
40
+ ''', (numero_processo, uf))
41
+ row = cursor.fetchone()
42
+ if not row:
43
+ conn.close()
44
+ return {"code": 404, "message": "Processo não encontrado."}
45
+ stored_credencial = row[0]
46
+ if stored_credencial != credencial:
47
+ conn.close()
48
+ return {"code": 403, "message": "Credencial inválida. Não autorizado para deletar este processo."}
49
+ # Credencial verificada; realiza a deleção
50
+ cursor.execute('''
51
+ DELETE FROM monitor_processos WHERE numero_processo = ? AND uf = ?
52
+ ''', (numero_processo, uf))
53
+ conn.commit()
54
+ conn.close()
55
+ return {"code": 200, "message": "Processo deletado com sucesso."}
56
+ except Exception as e:
57
+ return {"code": 500, "message": f"Erro ao deletar processo: {str(e)}"}
data_insert/inserir_advogado.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manipulacao de dados para o sistema de advogados, adicionar advogados
2
+
3
+ import sqlite3
4
+
5
+ DATABASE_FILE = "monitoramento.db"
6
+
7
+ def initialize_db():
8
+ conn = sqlite3.connect(DATABASE_FILE)
9
+ cursor = conn.cursor()
10
+ cursor.execute('''
11
+ CREATE TABLE IF NOT EXISTS monitor_advogados (
12
+ nome_advogado TEXT PRIMARY KEY,
13
+ credencial TEXT NOT NULL,
14
+ uf TEXT NOT NULL
15
+ )
16
+ ''')
17
+ conn.commit()
18
+ conn.close()
19
+
20
+ initialize_db()
21
+
22
+ async def insert_advogado(nome_advogado: str, uf: str, credencial: str):
23
+ """
24
+ Insere ou atualiza um nome de advogado no banco de dados de monitoramento.
25
+
26
+ Args:
27
+ nome_advogado (str): Nome do advogado a ser monitorado.
28
+ uf (str): Unidade federativa relacionada.
29
+ credencial (str): Credencial do responsável pela requisição.
30
+
31
+ Returns:
32
+ dict: Mensagem com o status da operação.
33
+ """
34
+ try:
35
+ conn = sqlite3.connect(DATABASE_FILE)
36
+ cursor = conn.cursor()
37
+ cursor.execute('''
38
+ INSERT OR REPLACE INTO monitor_advogados (nome_advogado, credencial, uf)
39
+ VALUES (?, ?, ?)
40
+ ''', (nome_advogado, credencial, uf))
41
+ conn.commit()
42
+ conn.close()
43
+ return {"code": 200, "message": "Advogado inserido com sucesso."}
44
+ except Exception as e:
45
+ return {"code": 500, "message": f"Erro ao inserir advogado: {str(e)}"}
data_insert/inserir_processo.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manipulação do banco de dados de adição de processos, adicionar do banco de dados
2
+
3
+ import sqlite3
4
+
5
+ DATABASE_FILE = "monitoramento.db"
6
+
7
+ def initialize_db():
8
+ conn = sqlite3.connect(DATABASE_FILE)
9
+ cursor = conn.cursor()
10
+ cursor.execute('''
11
+ CREATE TABLE IF NOT EXISTS monitor_processos (
12
+ numero_processo TEXT PRIMARY KEY,
13
+ credencial TEXT NOT NULL,
14
+ uf TEXT NOT NULL
15
+ )
16
+ ''')
17
+ conn.commit()
18
+ conn.close()
19
+
20
+ initialize_db()
21
+
22
+ async def insert_processo(numero_processo: str, uf: str, credencial: str):
23
+ """
24
+ Insere ou atualiza um número de processo no banco de dados de monitoramento.
25
+
26
+ Args:
27
+ numero_processo (str): Número do processo a ser monitorado.
28
+ uf (str): Unidade federativa relacionada.
29
+ credencial (str): Credencial do responsável pela requisição.
30
+
31
+ Returns:
32
+ dict: Mensagem com o status da operação.
33
+ """
34
+ try:
35
+ conn = sqlite3.connect(DATABASE_FILE)
36
+ cursor = conn.cursor()
37
+ cursor.execute('''
38
+ INSERT OR REPLACE INTO monitor_processos (numero_processo, credencial, uf)
39
+ VALUES (?, ?, ?)
40
+ ''', (numero_processo, credencial, uf))
41
+ conn.commit()
42
+ conn.close()
43
+ return {"code": 200, "message": "Processo inserido com sucesso."}
44
+ except Exception as e:
45
+ return {"code": 500, "message": f"Erro ao inserir processo: {str(e)}"}
monitoramento.db ADDED
Binary file (36.9 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohttp==3.9.5
2
+ aiosignal==1.3.2
3
+ annotated-types==0.6.0
4
+ anyio==4.8.0
5
+ attrs==23.2.0
6
+ bcrypt==3.2.0
7
+ beautifulsoup4==4.13.3
8
+ boto3==1.37.17
9
+ botocore==1.37.17
10
+ capmonstercloudclient==1.6.0
11
+ certifi==2025.1.31
12
+ cffi==1.17.1
13
+ charset-normalizer==3.4.1
14
+ click==8.1.8
15
+ colorama==0.4.6
16
+ coloredlogs==15.0.1
17
+ ddddocr==1.5.6
18
+ dnspython==2.7.0
19
+ ecdsa==0.19.1
20
+ email_validator==2.2.0
21
+ fastapi==0.110.1
22
+ filelock==3.17.0
23
+ flatbuffers==25.2.10
24
+ frozenlist==1.5.0
25
+ fsspec==2025.2.0
26
+ gradio_client==1.7.1
27
+ greenlet==3.1.1
28
+ gunicorn==23.0.0
29
+ h11==0.14.0
30
+ httpcore==1.0.7
31
+ httpx==0.28.1
32
+ huggingface-hub==0.29.1
33
+ humanfriendly==10.0
34
+ idna==3.10
35
+ jmespath==1.0.1
36
+ mpmath==1.3.0
37
+ multidict==6.0.5
38
+ onnxruntime==1.20.1
39
+ opencv-python-headless==4.11.0.86
40
+ packaging==24.2
41
+ passlib==1.7.4
42
+ pillow==11.1.0
43
+ propcache==0.3.0
44
+ protobuf==5.29.3
45
+ psycopg2==2.9.10
46
+ pyasn1==0.4.8
47
+ pycparser==2.22
48
+ pydantic==2.1.1
49
+ pydantic_core==2.4.0
50
+ pyreadline3==3.5.4
51
+ python-dateutil==2.9.0.post0
52
+ python-jose==3.4.0
53
+ PyYAML==6.0.2
54
+ requests==2.32.3
55
+ rsa==4.9
56
+ s3transfer==0.11.4
57
+ six==1.17.0
58
+ sniffio==1.3.1
59
+ soupsieve==2.6
60
+ SQLAlchemy==2.0.39
61
+ starlette==0.37.2
62
+ sympy==1.13.3
63
+ tqdm==4.67.1
64
+ typing_extensions==4.12.2
65
+ urllib3==2.3.0
66
+ uvicorn==0.29.0
67
+ websockets==14.2
68
+ yarl==1.18.3
server.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from os import environ as env
3
+ from fastapi.logger import logger as fastapi_logger
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse
6
+ from fastapi import FastAPI, Query, Depends, HTTPException, status, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+
9
+ # Local imports
10
+ from src import models, consulta
11
+ from src.monitoramento_processual.task_dispatcher import process_dispatcher
12
+ from data_delete.deletar_advogado import delete_advogado
13
+ from data_delete.deletar_processo import delete_processo
14
+ from data_insert.inserir_processo import insert_processo
15
+ from data_insert.inserir_advogado import insert_advogado
16
+ from user_autentication.user_hash import verificar_senha, criar_token, gerar_hash, verificar_token
17
+ from src.tj_research import tj_researcher
18
+ from src.models import UserUpdateUsername, UserUpdatePassword
19
+ from user_configuration.user_config import trocar_username, trocar_senha
20
+
21
+
22
+ import sqlite3
23
+ from pydantic import BaseModel
24
+
25
+ # Logger setup
26
+ msg_frt = "[%(asctime)s] %(levelname)s [%(name)s] - %(message)s"
27
+ time_frt = "%Y-%m-%d %H:%M:%S"
28
+ formatter = logging.Formatter(msg_frt, time_frt)
29
+ handler = logging.StreamHandler()
30
+ handler.setFormatter(formatter)
31
+ LOG_LEVEL = env.get('LOG_LEVEL', default='INFO')
32
+ fastapi_logger.addHandler(handler)
33
+ fastapi_logger.setLevel(LOG_LEVEL)
34
+
35
+ logger_name = env.get('BOT_NAME', default='Monitoramento Processual')
36
+ fastapi_logger.name = logger_name
37
+
38
+ # Dependencia para verificar token
39
+ def get_current_user(request: Request):
40
+ auth_header = request.headers.get('Authorization')
41
+ fastapi_logger.info(f"Authorization Header: {auth_header}")
42
+
43
+ if auth_header is None or not auth_header.startswith("Bearer "):
44
+ fastapi_logger.error("Authorization header ausente ou mal formado!")
45
+ raise HTTPException(
46
+ status_code=status.HTTP_401_UNAUTHORIZED,
47
+ detail="Token ausente"
48
+ )
49
+
50
+ token = auth_header.split(" ")[1]
51
+ fastapi_logger.info(f"Token extraído: {token}")
52
+
53
+ try:
54
+ username = verificar_token(token)
55
+ fastapi_logger.info(f"Usuário autenticado: {username}")
56
+ return username
57
+ except Exception as e:
58
+ fastapi_logger.error(f"Erro ao verificar token: {str(e)}")
59
+ raise HTTPException(
60
+ status_code=status.HTTP_401_UNAUTHORIZED,
61
+ detail="Token inválido ou expirado"
62
+ )
63
+
64
+ # API description e metadata
65
+ desc = '<a href="https://esaj.tjsp.jus.br/cpopg/search.do?conversationId=&cbPesquisa=NMADVOGADO&dadosConsulta.valorConsulta=&cdForo=-1" target="_blank">FONTE</a>'
66
+
67
+ tags_metadata = [
68
+ {
69
+ 'name': 'monitoramento-processual',
70
+ 'description': 'Consulta processual nos Tribunais de Justiça no Brasil',
71
+ }
72
+ ]
73
+
74
+ responses = {
75
+ 407: {'model': models.ResponseError, 'description': 'Proxy Authentication Required'},
76
+ 422: {'model': models.ResponseError, 'description': 'Unprocessable Entity'},
77
+ 500: {'model': models.ResponseError, 'description': 'Erro interno no servidor'},
78
+ 502: {'model': models.ResponseError, 'description': 'Bad Gateway'},
79
+ 504: {'model': models.ResponseError, 'description': 'Conexao com o site excedeu tempo limite'},
80
+ 509: {'model': models.ResponseError, 'description': 'Nao foi possivel resolver o captcha'},
81
+ 512: {'model': models.ResponseError, 'description': 'Erro ao executar parse da pagina'},
82
+ 513: {'model': models.ResponseError, 'description': 'Argumentos invalidos'}
83
+ }
84
+
85
+ app = FastAPI(
86
+ title='REST API - Monitoramento processual nos Tribunais de Justiça do Brasil',
87
+ description=desc,
88
+ debug=False,
89
+ openapi_tags=tags_metadata,
90
+ docs_url='/docs'
91
+ )
92
+
93
+ app.add_middleware(
94
+ CORSMiddleware,
95
+ allow_origins=['*'],
96
+ allow_credentials=True,
97
+ allow_methods=['*'],
98
+ allow_headers=['*'])
99
+
100
+ uf = Query(
101
+ description='UF referente ao advogado ou processo a ser consultado',
102
+ example='SP',
103
+ enum=['AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS', 'MG',
104
+ 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP',
105
+ 'SE', 'TO'],
106
+ default=None,
107
+ )
108
+
109
+ parametro_dummy = Query(
110
+ ...,
111
+ description='parametro dummy',
112
+ )
113
+
114
+ nome_advogado = Query(
115
+ ...,
116
+ description='Nome do advogado a ser inserido',
117
+ example='Nome do Advogado'
118
+ )
119
+
120
+ numero_processo = Query(
121
+ ...,
122
+ description='Número do processo a ser monitorado o andamento',
123
+ example='0000000-00.0000.0.00.0000'
124
+ )
125
+
126
+
127
+ # Informacoes do usuario
128
+ @app.get("/api/monitoramento-processual/usuario-info", tags=["monitoramento-processual"])
129
+ def get_user_info(username: str):
130
+ conn = sqlite3.connect("monitoramento.db")
131
+ cursor = conn.cursor()
132
+ cursor.execute("SELECT nome, sobrenome FROM usuarios WHERE username = ?", (username,))
133
+ row = cursor.fetchone()
134
+ conn.close()
135
+
136
+ if row:
137
+ return {"nome": row[0], "sobrenome": row[1]}
138
+ raise HTTPException(status_code=404, detail="Usuário não encontrado")
139
+
140
+ # Criação do banco de dados e tabelas
141
+ @app.get("/api/monitoramento-processual/consulta/listar-advogados", tags=["monitoramento-processual"])
142
+ def listar_advogados(current_user: str = Depends(get_current_user)):
143
+ conn = sqlite3.connect("monitoramento.db")
144
+ cursor = conn.cursor()
145
+ cursor.execute("SELECT nome_advogado, uf FROM monitor_advogados WHERE credencial = ?", (current_user,))
146
+ rows = cursor.fetchall()
147
+ conn.close()
148
+ return [{"nome_advogado": row[0], "uf": row[1]} for row in rows]
149
+
150
+ @app.get("/api/monitoramento-processual/consulta/listar-processos", tags=["monitoramento-processual"])
151
+ def listar_processos(current_user: str = Depends(get_current_user)):
152
+ conn = sqlite3.connect("monitoramento.db")
153
+ cursor = conn.cursor()
154
+ cursor.execute("SELECT numero_processo, uf FROM monitor_processos WHERE credencial = ?", (current_user,))
155
+ rows = cursor.fetchall()
156
+ conn.close()
157
+ return [{"numero_processo": row[0], "uf": row[1]} for row in rows]
158
+
159
+ @app.get("/api/monitoramento-processual/consulta/listar-tarefas", tags=["monitoramento-processual"])
160
+ def listar_tarefas(current_user: str = Depends(get_current_user)):
161
+ conn = sqlite3.connect("monitoramento.db")
162
+ cursor = conn.cursor()
163
+ cursor.execute("""
164
+ SELECT tipo, nome, uf, monitoramento, intervalo, unidade
165
+ FROM tarefas
166
+ WHERE username = ?
167
+ """, (current_user,))
168
+ rows = cursor.fetchall()
169
+ conn.close()
170
+ return [{
171
+ "tipo": row[0],
172
+ "nome": row[1],
173
+ "uf": row[2],
174
+ "monitoramento": row[3],
175
+ "intervalo": row[4],
176
+ "unidade": row[5]
177
+ } for row in rows]
178
+
179
+ # Retorna o HTML para o frontend
180
+ @app.get("/", tags=["monitoramento-processual"])
181
+ def serve_index():
182
+ return FileResponse("static/index.html")
183
+
184
+ @app.get("/dashboard", tags=["monitoramento-processual"])
185
+ def serve_dashboard():
186
+ return FileResponse("static/dashboard.html")
187
+
188
+ @app.get("/tasks", tags=["monitoramento-processual"])
189
+ def serve_tasks():
190
+ return FileResponse("static/tasks.html")
191
+
192
+ @app.get("/consulta", tags=["monitoramento-processual"])
193
+ def serve_consulta():
194
+ return FileResponse("static/consulta.html")
195
+
196
+ @app.get("/user_config", tags=["monitoramento-processual"])
197
+ def serve_user_config():
198
+ return FileResponse("static/user_config.html")
199
+
200
+
201
+ # Endpoints para consulta de processos e advogados
202
+
203
+ @app.delete("/api/monitoramento-processual/consulta/deletar-processo", tags=["monitoramento-processual"], responses=responses)
204
+ async def delete_processos(numero_processo: str = numero_processo, uf: str = uf, current_user: str = Depends(get_current_user)):
205
+ return await delete_processo(numero_processo, uf, current_user)
206
+
207
+ @app.delete("/api/monitoramento-processual/consulta/deletar-advogado", tags=["monitoramento-processual"], responses=responses)
208
+ async def delete_advogado_endpoint(nome_advogado: str = nome_advogado, uf: str = uf, current_user: str = Depends(get_current_user)):
209
+ return await delete_advogado(nome_advogado, uf, current_user)
210
+
211
+ @app.post("/api/monitoramento-processual/consulta/inserir-processo", tags=["monitoramento-processual"], responses=responses)
212
+ async def insert_processos_endpoint(numero_processo: str = numero_processo, uf: str = uf, current_user: str = Depends(get_current_user)):
213
+ return await insert_processo(numero_processo, uf, current_user)
214
+
215
+ @app.post("/api/monitoramento-processual/consulta/inserir-advogado", tags=["monitoramento-processual"], responses=responses)
216
+ async def insert_advogado_endpoint(nome_advogado: str = nome_advogado, uf: str = uf, current_user: str = Depends(get_current_user)):
217
+ return await insert_advogado(nome_advogado, uf, current_user)
218
+
219
+ @app.post("/api/monitoramento-processual/consulta/inserir-tarefa", tags=["monitoramento-processual"], responses=responses)
220
+ async def insert_tarefa(
221
+ tipo: str = Query(..., description="Tipo (advogado ou processo)"),
222
+ nome: str = Query(..., description="Nome do advogado ou número do processo"),
223
+ uf: str = Query(..., description="UF"),
224
+ monitoramento: str = Query(..., description="Deseja monitoramento? (sim ou nao)"),
225
+ intervalo: int = Query(..., description="Intervalo do monitoramento"),
226
+ unidade: str = Query(..., description="Unidade do intervalo (minutos, horas ou dias)"),
227
+ current_user: str = Depends(get_current_user)
228
+ ):
229
+ conn = sqlite3.connect("monitoramento.db")
230
+ cursor = conn.cursor()
231
+ cursor.execute('''
232
+ CREATE TABLE IF NOT EXISTS tarefas (
233
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
234
+ username TEXT NOT NULL,
235
+ tipo TEXT NOT NULL,
236
+ nome TEXT NOT NULL,
237
+ uf TEXT NOT NULL,
238
+ monitoramento TEXT NOT NULL,
239
+ intervalo INTEGER NOT NULL,
240
+ unidade TEXT NOT NULL
241
+ )
242
+ ''')
243
+ cursor.execute('''
244
+ INSERT INTO tarefas (username, tipo, nome, uf, monitoramento, intervalo, unidade)
245
+ VALUES (?, ?, ?, ?, ?, ?, ?)
246
+ ''', (current_user, tipo, nome, uf, monitoramento, intervalo, unidade))
247
+ conn.commit()
248
+ conn.close()
249
+ return {"message": "Tarefa cadastrada com sucesso"}
250
+
251
+
252
+ @app.get("/api/monitoramento-processual/consulta/tjs", tags=["monitoramento-processual"], responses=responses)
253
+ async def get_consulta_tj(
254
+ uf: str = Query(..., description="Unidade Federativa"),
255
+ process_number: str = Query(..., description="Número do processo"),
256
+ current_user: str = Depends(get_current_user)
257
+ ):
258
+ result = await tj_researcher.research_process(uf=uf, process_number=process_number)
259
+ return result
260
+
261
+ @app.get("/api/monitoramento-processual/monitoramento/uf", tags=["monitoramento-processual"], responses=responses)
262
+ async def get_consulta_tjsc(uf: str = uf, current_user: str = Depends(get_current_user)):
263
+ return await process_dispatcher(current_user, uf)
264
+
265
+ app.mount("/static", StaticFiles(directory="static", html=True), name="static")
266
+
267
+ class LoginData(BaseModel):
268
+ username: str
269
+ senha: str
270
+
271
+ @app.post("/api/monitoramento-processual/login", tags=["monitoramento-processual"])
272
+ def login(data: LoginData):
273
+ conn = sqlite3.connect("monitoramento.db")
274
+ cursor = conn.cursor()
275
+ cursor.execute("SELECT senha_hash FROM usuarios WHERE username = ?", (data.username,))
276
+ row = cursor.fetchone()
277
+ conn.close()
278
+ if not row or not verificar_senha(data.senha, row[0]):
279
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Usuário ou senha inválidos")
280
+ token = criar_token({"sub": data.username})
281
+ return {"access_token": token, "token_type": "bearer"}
282
+
283
+ @app.post("/api/monitoramento-processual/cadastrar-usuario", tags=["monitoramento-processual"])
284
+ def cadastrar_usuario(data: models.UserCreate):
285
+ conn = sqlite3.connect("monitoramento.db")
286
+ cursor = conn.cursor()
287
+ senha_hash = gerar_hash(data.senha)
288
+ try:
289
+ cursor.execute(
290
+ "INSERT INTO usuarios (username, senha_hash, nome, sobrenome, email) VALUES (?, ?, ?, ?, ?)",
291
+ (data.username, senha_hash, data.nome, data.sobrenome, data.email)
292
+ )
293
+ conn.commit()
294
+ return {"message": "Usuário cadastrado com sucesso."}
295
+ except sqlite3.IntegrityError:
296
+ raise HTTPException(
297
+ status_code=status.HTTP_400_BAD_REQUEST,
298
+ detail="Usuário já existe."
299
+ )
300
+ finally:
301
+ conn.close()
302
+
303
+
304
+ @app.put("/api/monitoramento-processual/config/usuario/username", tags=["monitoramento-processual"])
305
+ async def atualizar_username(data: UserUpdateUsername, current_user: str = Depends(get_current_user)):
306
+ return await trocar_username(current_user, data.new_username)
307
+
308
+ @app.put("/api/monitoramento-processual/config/usuario/password", tags=["monitoramento-processual"])
309
+ async def atualizar_senha(data: UserUpdatePassword, current_user: str = Depends(get_current_user)):
310
+ return await trocar_senha(current_user, data.current_password, data.new_password)
src/consulta.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from os import environ as env
3
+ from datetime import datetime, timedelta
4
+ import aiohttp
5
+ from bs4 import BeautifulSoup
6
+ from fastapi.logger import logger
7
+ from fastapi.responses import JSONResponse
8
+ from fastapi import status
9
+
10
+ # Local imports
11
+ from src.models import *
12
+ from src.processamento_tjdf import tjdf_request
13
+ from src.enviador_de_email import enviar_email
14
+ from utils.util import get_headers
15
+
16
+ # Captura variáveis de ambiente e cria constantes
17
+ TIMEOUT = env.get('TIMEOUT', default=180)
18
+
19
+ def get_advogados_by_credencial(credencial: str, uf: str) -> list:
20
+ """
21
+ Consulta o banco de dados monitoramento.db para obter os nomes dos advogados
22
+ cadastrados para a credencial e UF informadas.
23
+
24
+ Returns:
25
+ list: Lista de nomes de advogados (no formato armazenado, por exemplo, com '+' para espaços, se necessário)
26
+ """
27
+ try:
28
+ conn = sqlite3.connect("monitoramento.db")
29
+ cursor = conn.cursor()
30
+ cursor.execute("SELECT nome_advogado FROM monitor_advogados WHERE credencial = ? AND uf = ?", (credencial, uf))
31
+ rows = cursor.fetchall()
32
+ conn.close()
33
+ return [row[0] for row in rows]
34
+ except Exception as e:
35
+ logger.error(f"Erro ao buscar advogados no banco: {str(e)}")
36
+ return []
37
+
38
+ async def processamento_lista_soup(soup, nome_advogado, grau):
39
+ """ Extrai processos do HTML do TJSP usando BeautifulSoup """
40
+ try:
41
+ processos = []
42
+ # Itera sobre todos os processos listados
43
+ for processo in soup.select("li div.row.unj-ai-c"):
44
+ # Número do processo
45
+ link_element = processo.select_one("a.linkProcesso")
46
+ numero_processo = link_element.text.strip() if link_element else ''
47
+ link_processo = f"https://esaj.tjsp.jus.br{link_element['href']}" if link_element else ''
48
+
49
+ # Nome do Advogado
50
+ nome_advogado_element = processo.select_one(".unj-base-alt")
51
+ nome_advogado = nome_advogado_element.text.strip() if nome_advogado_element else nome_advogado.replace("+", " ")
52
+
53
+ # Tipo de Processo (classe do processo)
54
+ tipo_processo_element = processo.select_one(".classeProcesso")
55
+ tipo_processo = tipo_processo_element.text.strip() if tipo_processo_element else ''
56
+
57
+ # Assunto do Processo
58
+ assunto_processo_element = processo.select_one(".assuntoPrincipalProcesso, .assuntoProcesso")
59
+ assunto_processo = assunto_processo_element.text.strip() if assunto_processo_element else ''
60
+
61
+ # Data de recebimento
62
+ data_recebimento_element = processo.select_one(".dataLocalDistribuicaoProcesso, .dataLocalDistribuicao")
63
+ data_recebimento = data_recebimento_element.text.strip() if data_recebimento_element else ''
64
+
65
+ # Foro do Processo
66
+ foro_element = processo.find_previous("h2", class_="unj-subtitle")
67
+ foro_processo = foro_element.text.strip() if foro_element else ''
68
+
69
+ processos.append({
70
+ "nome_advogado": nome_advogado,
71
+ "numero_processo": numero_processo,
72
+ "tipo_processo": tipo_processo,
73
+ "assunto_processo": assunto_processo,
74
+ "data_recebimento": data_recebimento,
75
+ "link_processo": link_processo,
76
+ "foro_processo": foro_processo,
77
+ "grau_processo": grau
78
+ })
79
+
80
+ return processos
81
+
82
+ except Exception as e:
83
+ logger.error(f"Erro ao processar HTML: {str(e)}")
84
+ return []
85
+
86
+ #-----------------------------------------------------------------------------------------------------
87
+ async def fetch(parametro_dummy: str, credencial_usuario: str, uf: str):
88
+ """
89
+ Executa uma consulta genérica (dummy) de monitoramento processual.
90
+
91
+ Ao invés de usar uma lista fixa de nomes, obtém a lista de advogados cadastrados no banco
92
+ para o usuário identificado pela credencial e UF informados.
93
+
94
+ Args:
95
+ parametro_dummy (str): Parâmetro dummy que ativa a consulta.
96
+ credencial_usuario (str): Credencial do usuário solicitante.
97
+ uf (str): Unidade Federativa relacionada à consulta.
98
+
99
+ Returns:
100
+ dict: Resultado da consulta processual.
101
+ """
102
+ logger.info(f"Consulta dummy iniciada para credencial: {credencial_usuario}")
103
+
104
+ # Configura os timeouts
105
+ timeout = aiohttp.ClientTimeout(total=TIMEOUT)
106
+ session = aiohttp.ClientSession(timeout=timeout)
107
+
108
+ # Configurando headers
109
+ session.headers.update(get_headers())
110
+
111
+ # Obtém a lista de advogados cadastrados para o usuário
112
+ lista_nomes = get_advogados_by_credencial(credencial_usuario, uf)
113
+ if not lista_nomes:
114
+ await session.close()
115
+ return JSONResponse(
116
+ status_code=status.HTTP_404_NOT_FOUND,
117
+ content={"code": 404, "message": "Nenhum advogado cadastrado para essa credencial.", "datetime": datetime.now().isoformat()}
118
+ )
119
+
120
+ try:
121
+ resultados = []
122
+
123
+ for nome_advogado in lista_nomes:
124
+ # URLs de consulta para TJSP (primeira e segunda instância)
125
+ url_primeiro_grau = f"https://esaj.tjsp.jus.br/cpopg/search.do?conversationId=&cbPesquisa=NMADVOGADO&dadosConsulta.valorConsulta={nome_advogado}&cdForo=-1"
126
+ url_segundo_grau = f"https://esaj.tjsp.jus.br/cposg/search.do?conversationId=&paginaConsulta=0&cbPesquisa=NMADVOGADO&dePesquisa={nome_advogado}&localPesquisa.cdLocal=-1"
127
+
128
+ logger.info(f"Consultando processos para: {nome_advogado.replace('+', ' ')}")
129
+
130
+ # Primeira Instância
131
+ async with session.get(url_primeiro_grau, ssl=False, allow_redirects=True) as resp1:
132
+ html_primeiro_grau = await resp1.read()
133
+ soup_primeiro_grau = BeautifulSoup(html_primeiro_grau, "html.parser")
134
+ if "Não existem informações disponíveis para os parâmetros informados." not in soup_primeiro_grau.text:
135
+ processos_primeiro_grau = await processamento_lista_soup(soup_primeiro_grau, nome_advogado.replace('+', ' '), "1º Grau")
136
+ resultados.extend(processos_primeiro_grau)
137
+
138
+ # Segunda Instância
139
+ async with session.get(url_segundo_grau, ssl=False, allow_redirects=True) as resp2:
140
+ html_segundo_grau = await resp2.read()
141
+ soup_segundo_grau = BeautifulSoup(html_segundo_grau, "html.parser")
142
+ if "Não existem informações disponíveis para os parâmetros informados." not in soup_segundo_grau.text:
143
+ processos_segundo_grau = await processamento_lista_soup(soup_segundo_grau, nome_advogado.replace('+', ' '), "2º Grau")
144
+ resultados.extend(processos_segundo_grau)
145
+
146
+ # Requisição ao TJDF
147
+ processos_tjdf = await tjdf_request(nome_advogado)
148
+ resultados.extend(processos_tjdf)
149
+
150
+ processos_48h = []
151
+ now = datetime.now()
152
+
153
+ for proc in resultados:
154
+ try:
155
+ data_str = proc['data_recebimento'].split(" - ")[0].strip()
156
+ dt = datetime.strptime(data_str, "%d/%m/%Y")
157
+
158
+ # Filtra processos dos últimos 2 dias
159
+ if dt >= now - timedelta(days=2):
160
+ proc['data_recebimento_dt'] = dt
161
+ processos_48h.append(proc)
162
+ except Exception as e:
163
+ logger.error(f"Erro ao converter data: {str(e)}")
164
+ continue
165
+
166
+ processos_48h.sort(key=lambda x: x['data_recebimento_dt'], reverse=True)
167
+
168
+ for proc in processos_48h:
169
+ proc.pop('data_recebimento_dt', None)
170
+
171
+ # Envia o e-mail mesmo se nenhum processo for encontrado
172
+ try:
173
+ sucesso = await enviar_email(processos_48h)
174
+ if sucesso:
175
+ logger.info("E-mail enviado com sucesso!")
176
+ else:
177
+ logger.error("Falha ao enviar o e-mail.")
178
+ except Exception as e:
179
+ logger.exception(f"Erro inesperado ao enviar e-mail: {str(e)}")
180
+
181
+ result = {
182
+ 'code': 200,
183
+ 'message': 'OK',
184
+ 'datetime': datetime.now().isoformat(),
185
+ 'results': processos_48h
186
+ }
187
+
188
+ except aiohttp.ClientError as e:
189
+ logger.exception('Erro durante a consulta API')
190
+ return JSONResponse(
191
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
192
+ content={'code': 500, 'message': f'INTERNAL_SERVER_ERROR: {str(e)}'}
193
+ )
194
+ except Exception as e:
195
+ logger.exception('Erro inesperado durante a consulta API')
196
+ return JSONResponse(
197
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
198
+ content={'code': 500, 'message': f'INTERNAL_SERVER_ERROR: {str(e)}'}
199
+ )
200
+
201
+ logger.info(f"Consulta finalizada: {result}")
202
+ await session.close()
203
+ return result
src/enviador_de_email.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import smtplib
2
+ import email.message
3
+ import asyncio
4
+ from fastapi.logger import logger
5
+
6
+ async def enviar_email_movimentacao(ultima_movimentacao_texto, numero_processo):
7
+ """
8
+ Envia um e-mail informando o andamento de um processo específico,
9
+ mesmo quando não houver atualização.
10
+
11
+ O parâmetro 'ultima_movimentacao_texto' pode vir em dois formatos, por exemplo:
12
+ 1) "87 15/09/2023 18:46:46 Despacho https://eprocwebcon.tjsc.jus.br/consulta1g/controlador.php?acao=acessar_documento_publico&doc=...&hash=..."
13
+ 2) "88 15/09/2023 18:46:47 Expedida/certificada a intimação eletrônicaRefer. ao Evento 87(EXEQUENTE - MUNICÍPIO DE TIMBÓ/SC)Prazo: 30 dias Status:FECHADOData inicial da contagem do prazo: 26/09/2023 00:00:00Data final: 14/11/2023 23:59:59"
14
+
15
+ Se o texto estiver vazio ou conter apenas espaços, o e-mail informará que não houve atualização.
16
+
17
+ Caso haja conteúdo, a função realiza o parsing dos três primeiros tokens (código, data e hora)
18
+ e utiliza o restante como descrição. Se houver uma substring iniciada por "http", ela será extraída como link do documento.
19
+
20
+ Args:
21
+ ultima_movimentacao_texto (str): Texto com os detalhes do último andamento.
22
+ numero_processo (str): Número do processo.
23
+
24
+ Returns:
25
+ bool: True se o e-mail for enviado com sucesso, False caso contrário.
26
+ """
27
+ if not ultima_movimentacao_texto or not ultima_movimentacao_texto.strip():
28
+ corpo_email = f"""
29
+ <html>
30
+ <body>
31
+ <h2>Andamento do Processo</h2>
32
+ <p><strong>Número do Processo:</strong> {numero_processo}</p>
33
+ <p>Não houve atualização no andamento deste processo.</p>
34
+ <p>Atenciosamente,<br>Equipe LexAutomation</p>
35
+ </body>
36
+ </html>
37
+ """
38
+ else:
39
+ try:
40
+ # Tenta extrair um link, se houver
41
+ pos_link = ultima_movimentacao_texto.find("http")
42
+ if pos_link != -1:
43
+ link = ultima_movimentacao_texto[pos_link:].strip()
44
+ texto_sem_link = ultima_movimentacao_texto[:pos_link].strip()
45
+ partes = texto_sem_link.split(maxsplit=3)
46
+ else:
47
+ link = ""
48
+ partes = ultima_movimentacao_texto.split(maxsplit=3)
49
+
50
+ if len(partes) < 4:
51
+ codigo_evento = partes[0] if len(partes) > 0 else "N/A"
52
+ data_evento = partes[1] if len(partes) > 1 else "N/A"
53
+ hora_evento = partes[2] if len(partes) > 2 else "N/A"
54
+ descricao_evento = ""
55
+ else:
56
+ codigo_evento, data_evento, hora_evento, descricao_evento = partes
57
+ except Exception as e:
58
+ logger.error(f"Erro ao realizar o parsing da movimentação: {e}")
59
+ codigo_evento = "N/A"
60
+ data_evento = "N/A"
61
+ hora_evento = "N/A"
62
+ descricao_evento = ultima_movimentacao_texto
63
+ link = ""
64
+
65
+ documento = f'<a href="{link}">Acessar Documento</a>' if link else "Nenhum documento"
66
+ corpo_email = f"""
67
+ <html>
68
+ <body>
69
+ <h2>Atualização de Andamento do Processo</h2>
70
+ <p><strong>Número do Processo:</strong> {numero_processo}</p>
71
+ <p><strong>Código do Evento:</strong> {codigo_evento}</p>
72
+ <p><strong>Data e Hora:</strong> {data_evento} {hora_evento}</p>
73
+ <p><strong>Descrição:</strong> {descricao_evento}</p>
74
+ <p><strong>Documento:</strong> {documento}</p>
75
+ <p>Atenciosamente,<br>Equipe LexAutomation</p>
76
+ </body>
77
+ </html>
78
+ """
79
+
80
+ # Configuração do e-mail
81
+ remetente = "processosatualizacao@gmail.com"
82
+ destinatarios = [
83
+ "brun.rreis@gmail.com",
84
+ "fontaosthefano@gmail.com"
85
+ ]
86
+ senha = "awbz sfki ndtd afwb"
87
+
88
+ def enviar():
89
+ try:
90
+ with smtplib.SMTP("smtp.gmail.com", 587) as server:
91
+ server.starttls()
92
+ server.login(remetente, senha)
93
+ for destinatario in destinatarios:
94
+ msg = email.message.Message()
95
+ msg['Subject'] = f"Atualização de Andamento - Processo {numero_processo}"
96
+ msg['From'] = remetente
97
+ msg['To'] = destinatario
98
+ msg.add_header('Content-Type', 'text/html')
99
+ msg.set_payload(corpo_email)
100
+
101
+ server.sendmail(remetente, destinatario, msg.as_string().encode("utf-8"))
102
+ logger.info("E-mail enviado com sucesso!")
103
+ return True
104
+ except Exception as e:
105
+ logger.info(f"Erro ao enviar e-mail: {str(e)}")
106
+ return False
107
+
108
+ return await asyncio.to_thread(enviar)
109
+
110
+
111
+ async def enviar_email(resultados):
112
+ """ Envia um e-mail com a lista de processos atualizada """
113
+
114
+ # Configuração do e-mail
115
+ remetente = "processosatualizacao@gmail.com"
116
+ destinatarios = [
117
+ "brun.rreis@gmail.com",
118
+ "fontaosthefano@gmail.com"
119
+ ]
120
+ senha = "awbz sfki ndtd afwb"
121
+
122
+
123
+ if not resultados:
124
+ corpo_email = """
125
+ <html>
126
+ <body>
127
+ <h3>🤖 Monitoramento de Processos</h3>
128
+ <p>Olá,</p>
129
+ <p>Informamos que, no período de tempo selecionado, não foram identificados novos processos.</p>
130
+ <p>Atenciosamente,<br>
131
+ Equipe LexAutomation</p>
132
+ </body>
133
+ </html>
134
+ """
135
+ else:
136
+ corpo_email = """
137
+ <html>
138
+ <body>
139
+ <h2>🤖 Monitoramento de Processos</h2>
140
+ <h3>Atualização diária de processos</h3>
141
+ <ul>
142
+ """
143
+ for processo in resultados:
144
+ corpo_email += f"""
145
+ <li>
146
+ <b>Nome do Advogado:</b> {processo['nome_advogado']}<br>
147
+ <b>Número do Processo:</b> {processo['numero_processo']}<br>
148
+ <b>Tipo do Processo:</b> {processo['tipo_processo']}<br>
149
+ <b>Assunto:</b> {processo['assunto_processo']}<br>
150
+ <b>Data de Recebimento:</b> {processo.get('data_recebimento', 'N/A')}<br>
151
+ <b>Foro:</b> {processo['foro_processo']}<br>
152
+ <b>Grau:</b> {processo['grau_processo']}<br>
153
+ <b>Link:</b> <a href="{processo['link_processo']}">Acessar Processo</a><br>
154
+ </li>
155
+ <hr>
156
+ """
157
+ corpo_email += """
158
+ </ul>
159
+ <p>Atenciosamente,<br>
160
+ Equipe LexAutomation</p>
161
+ </body>
162
+ </html>
163
+ """
164
+
165
+
166
+ def enviar():
167
+ try:
168
+ with smtplib.SMTP("smtp.gmail.com", 587) as server:
169
+ server.starttls()
170
+ server.login(remetente, senha)
171
+ for destinatario in destinatarios:
172
+ msg = email.message.Message()
173
+ msg['Subject'] = "Atualização diária de processos"
174
+ msg['From'] = remetente
175
+ msg['To'] = destinatario
176
+ msg.add_header('Content-Type', 'text/html')
177
+ msg.set_payload(corpo_email)
178
+
179
+ server.sendmail(remetente, destinatario, msg.as_string().encode("utf-8"))
180
+ logger.info("E-mail enviado com sucesso!")
181
+ return True
182
+ except Exception as e:
183
+ logger.info(f"Erro ao enviar e-mail: {str(e)}")
184
+ return False
185
+
186
+ return await asyncio.to_thread(enviar)
src/models.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+ from typing import Optional, List, Union
3
+
4
+ # -------------------- TJPR --------------------
5
+ class MovimentacaoTJPR(BaseModel):
6
+ seq: Optional[str] = ""
7
+ data: Optional[str] = ""
8
+ evento: Optional[str] = ""
9
+ movimentado_por: Optional[str] = ""
10
+
11
+ class ResponseSiteTJPR(BaseModel):
12
+ movimentacoes: List[MovimentacaoTJPR] = []
13
+
14
+ # -------------------- TJSP --------------------
15
+ class MovimentacaoTJSP(BaseModel):
16
+ data_hora: str = ""
17
+ descricao: str = ""
18
+ documentos: str = ""
19
+
20
+ class ResponseSiteTJSP(BaseModel):
21
+ movimentacoes: List[MovimentacaoTJSP] = []
22
+
23
+ # -------------------- TJSC --------------------
24
+ class MovimentacaoTJSC(BaseModel):
25
+ evento: str = ""
26
+ data_hora: str = ""
27
+ descricao: str = ""
28
+ documentos: str = ""
29
+
30
+ class ResponseSiteTJSC(BaseModel):
31
+ movimentacoes: List[MovimentacaoTJSC] = []
32
+
33
+ # -------------------- Union Simplificada --------------------
34
+ Movimentacao = Union[MovimentacaoTJPR, MovimentacaoTJSP, MovimentacaoTJSC]
35
+
36
+ # -------------------- Genéricos --------------------
37
+ class UserUpdateUsername(BaseModel):
38
+ new_username: str
39
+
40
+ class UserUpdatePassword(BaseModel):
41
+ current_password: str
42
+ new_password: str
43
+
44
+ class Telemetria(BaseModel):
45
+ tentativas: int
46
+ tempo_total: float = 0.0
47
+ bytes_enviados: int = 0
48
+ captchas_resolvidos: int = 0
49
+
50
+ class ResponseError(BaseModel):
51
+ code: int
52
+ message: str
53
+
54
+ class ResponseDefault(BaseModel):
55
+ code: int
56
+ message: str
57
+ datetime: str
58
+
59
+ class UserCreate(BaseModel):
60
+ username: str
61
+ senha: str
62
+ nome: str
63
+ sobrenome: str
64
+ email: EmailStr
src/monitoramento_processual/monitoramento_tjpr.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import re
3
+ import logging
4
+ from datetime import datetime
5
+ from os import environ as env
6
+
7
+ import httpx
8
+ from fastapi import status
9
+ from fastapi.responses import JSONResponse
10
+ from bs4 import BeautifulSoup
11
+ from gradio_client import Client, handle_file
12
+ import ddddocr
13
+
14
+ # Imports locais
15
+ from src.models import ResponseSiteTJPR, Telemetria, MovimentacaoTJPR
16
+ import src.models as models
17
+
18
+ # Configuração do Logger
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
22
+ datefmt="%Y-%m-%d %H:%M:%S"
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Constantes
27
+ TEMPO_LIMITE = int(env.get('TEMPO_LIMITE', 180))
28
+ TENTATIVAS_MAXIMAS_CAPTCHA = int(env.get('TENTATIVAS_MAXIMAS_CAPTCHA', 30))
29
+ TENTATIVAS_MAXIMAS_RECURSIVAS = int(env.get('TENTATIVAS_MAXIMAS_RECURSIVAS', 30))
30
+ BASE_URL = "https://consulta.tjpr.jus.br"
31
+
32
+
33
+ async def extrair_movimentacoes(soup: BeautifulSoup) -> ResponseSiteTJPR:
34
+ movimentacoes = []
35
+ tabelas = soup.find_all("table", {"class": "resultTable"})
36
+ for tabela in tabelas:
37
+ linhas = tabela.find_all("tr")[1:]
38
+ for linha in linhas:
39
+ colunas = linha.find_all("td")
40
+ if len(colunas) >= 5:
41
+ seq = colunas[1].get_text(strip=True)
42
+ data = colunas[2].get_text(strip=True)
43
+ evento = " ".join(colunas[3].stripped_strings)
44
+
45
+ nome = colunas[4].contents[2].strip() if len(colunas[4].contents) > 2 else ""
46
+ cargo_tag = colunas[4].find("b")
47
+ cargo = cargo_tag.get_text(strip=True) if cargo_tag else ""
48
+
49
+ if not nome:
50
+ nome_tag = colunas[4].find(text=True, recursive=False)
51
+ nome = nome_tag.strip() if nome_tag else ""
52
+
53
+ movimentado_por = f"{nome} - {cargo}" if nome and cargo else (nome or cargo or colunas[4].get_text(strip=True))
54
+ movimentacoes.append(MovimentacaoTJPR(seq=seq, data=data, evento=evento, movimentado_por=movimentado_por))
55
+
56
+ return ResponseSiteTJPR(movimentacoes=movimentacoes)
57
+
58
+
59
+
60
+ def formatar_numero_processo(numero_processo: str) -> str:
61
+ """
62
+ Remove quaisquer caracteres não numéricos do número do processo.
63
+ """
64
+ return ''.join(filter(str.isdigit, numero_processo))
65
+
66
+
67
+ async def obter_soup(client: httpx.AsyncClient, url: str) -> BeautifulSoup:
68
+ """Realiza GET na URL e retorna o BeautifulSoup do conteúdo."""
69
+ resposta = await client.get(url)
70
+ if resposta.status_code != 200:
71
+ raise Exception(f"Falha ao acessar {url}: {resposta.status_code}")
72
+ return BeautifulSoup(resposta.text, "html.parser")
73
+
74
+
75
+ async def resolver_captcha(client: httpx.AsyncClient, url_captcha: str, telemetria: Telemetria) -> str:
76
+ """Faz download e resolve o CAPTCHA utilizando API e fallback OCR."""
77
+ resposta_captcha = await client.get(url_captcha)
78
+ if resposta_captcha.status_code != 200:
79
+ raise Exception(f"Falha ao baixar o CAPTCHA: {resposta_captcha.status_code}")
80
+ bytes_imagem = resposta_captcha.content
81
+ with open("captcha_temporario.png", "wb") as arquivo:
82
+ arquivo.write(bytes_imagem)
83
+
84
+ cliente_api = Client("Nischay103/captcha_recognition")
85
+ telemetria.captchas_resolvidos += 1
86
+ try:
87
+ resultado_ocr = cliente_api.predict(
88
+ input=handle_file("captcha_temporario.png"),
89
+ api_name="/predict"
90
+ ).strip()
91
+ logger.info(f"CAPTCHA reconhecido pela API: {resultado_ocr}")
92
+ except Exception as e:
93
+ logger.error(f"Erro ao usar a API captcha_recognition: {e}")
94
+ try:
95
+ motor_ocr = ddddocr.DdddOcr()
96
+ resultado_ocr = motor_ocr.classification(bytes_imagem)
97
+ logger.info(f"CAPTCHA reconhecido pelo ddddocr (fallback): {resultado_ocr}")
98
+ except Exception as fallback_e:
99
+ logger.error(f"Erro no fallback ddddocr: {fallback_e}")
100
+ raise
101
+ return resultado_ocr
102
+
103
+
104
+ def extrair_url_token(script_tags: list, pattern_str: str) -> str:
105
+ """Extrai uma URL/token utilizando regex a partir dos scripts."""
106
+ pattern = re.compile(pattern_str)
107
+ for script in script_tags:
108
+ if script.string:
109
+ match = pattern.search(script.string)
110
+ if match:
111
+ token = match.group(1)
112
+ if "_tj=" in token:
113
+ return token
114
+ return None
115
+
116
+
117
+ async def fetch(numero_processo: str, telemetria: Telemetria) -> dict:
118
+ """
119
+ Função principal que realiza a consulta. Mantém a estrutura original de tentativas
120
+ e de recursão para resolver o CAPTCHA, e retorna um dicionário com os resultados e telemetria.
121
+ """
122
+ inicio_tempo = time.time()
123
+ if not numero_processo or not isinstance(numero_processo, str):
124
+ telemetria.tempo_total = round(time.time() - inicio_tempo, 2)
125
+ return JSONResponse(
126
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
127
+ content={'code': 2, 'message': 'ERRO_ENTIDADE_NAO_PROCESSAVEL'}
128
+ )
129
+
130
+ if telemetria.tentativas >= TENTATIVAS_MAXIMAS_RECURSIVAS:
131
+ logger.error("Número máximo de tentativas recursivas atingido.")
132
+ return JSONResponse(
133
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
134
+ content={'code': 3, 'message': 'ERRO_SERVIDOR_INTERNO'}
135
+ )
136
+
137
+ logger.info(f'Função fetch() iniciou. Processo: {numero_processo} - Tentativa {telemetria.tentativas}')
138
+ headers = {
139
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
140
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
141
+ 'Accept-Language': 'en-US,en;q=0.5',
142
+ 'Referer': f'{BASE_URL}/projudi_consulta/'
143
+ }
144
+
145
+ results = {}
146
+ try:
147
+ async with httpx.AsyncClient(timeout=TEMPO_LIMITE, headers=headers, follow_redirects=True) as client:
148
+ # 1º GET: Página inicial
149
+ url_pagina_consulta = f"{BASE_URL}/projudi_consulta/"
150
+ resposta_inicial = await client.get(url_pagina_consulta)
151
+ telemetria.bytes_enviados += len(resposta_inicial.text.encode('utf-8'))
152
+ if resposta_inicial.status_code != 200:
153
+ raise Exception(f"Falha ao acessar a página inicial: {resposta_inicial.status_code}")
154
+
155
+ soup_inicial = BeautifulSoup(resposta_inicial.text, "html.parser")
156
+ frame = soup_inicial.find("frame", {"id": "mainFrame"})
157
+ if not (frame and frame.get("src")):
158
+ raise Exception("Frame com id='mainFrame' não encontrado ou sem atributo 'src'")
159
+ src = re.sub(r';jsessionid=[^?]*', '', frame["src"])
160
+ url_final = f"{BASE_URL}{src}"
161
+ logger.info(f"URL do mainFrame: {url_final}")
162
+
163
+ # 2º GET: cabecalho.jsp
164
+ url_cabecalho = f"{BASE_URL}/projudi_consulta/cabecalho.jsp"
165
+ resposta_cabecalho = await client.get(url_cabecalho)
166
+ telemetria.bytes_enviados += len(resposta_cabecalho.text.encode('utf-8'))
167
+ if resposta_cabecalho.status_code != 200:
168
+ raise Exception(f"Falha ao acessar cabecalho.jsp: {resposta_cabecalho.status_code}")
169
+
170
+ client.headers.update({'Referer': url_final})
171
+
172
+ # 3º GET: Página de consulta pública (mainFrame)
173
+ resposta_consulta = await client.get(url_final)
174
+ telemetria.bytes_enviados += len(resposta_consulta.text.encode('utf-8'))
175
+ if resposta_consulta.status_code != 200:
176
+ raise Exception(f"Falha ao acessar a página de consulta: {resposta_consulta.status_code}")
177
+
178
+ soup_consulta = BeautifulSoup(resposta_consulta.text, "html.parser")
179
+ # Captura da imagem do CAPTCHA
180
+ captcha_img_elem = soup_consulta.find("img", {"id": "captchaImage"})
181
+ if not (captcha_img_elem and captcha_img_elem.get("src")):
182
+ raise Exception("Imagem do CAPTCHA não encontrada ou sem atributo 'src'!")
183
+ url_captcha = f"{BASE_URL}{captcha_img_elem['src']}"
184
+ logger.info(f"URL do CAPTCHA: {url_captcha}")
185
+
186
+ # Captura URL AJAX para Autocomplete e demais chamadas
187
+ script_tags = soup_consulta.find_all("script", {"type": "text/javascript"})
188
+ url_ajax = extrair_url_token(script_tags, r'AjaxJspTag\.Select\(\s*"([^"]+)"')
189
+ if not url_ajax:
190
+ raise Exception("URL com '_tj=' não encontrada no HTML")
191
+ if "codComarca" not in url_ajax:
192
+ url_ajax += ("&" if "?" in url_ajax else "?") + "codComarca=-1"
193
+ url_final_ajax = url_ajax if url_ajax.startswith("http") else f"{BASE_URL}{url_ajax}"
194
+ resp_ajax = await client.get(url_final_ajax)
195
+ telemetria.bytes_enviados += len(resp_ajax.text.encode('utf-8'))
196
+ if resp_ajax.status_code != 200:
197
+ raise Exception(f"Falha ao acessar a URL AJAX: {resp_ajax.status_code}")
198
+
199
+ url_autocomplete = extrair_url_token(script_tags, r'AjaxJspTag\.Autocomplete\(\s*"([^"]+)"')
200
+ if not url_autocomplete:
201
+ raise Exception("URL do Autocomplete com '_tj=' não encontrada no HTML")
202
+ segunda_url_ajax = url_autocomplete if url_autocomplete.startswith("http") else f"{BASE_URL}{url_autocomplete}"
203
+
204
+ payload_segundo_ajax = {
205
+ "numeroProcesso": numero_processo,
206
+ "flagNumeroUnico": "true",
207
+ "opcaoConsulta": "1",
208
+ "_": ""
209
+ }
210
+ ajax_headers = {
211
+ "x-prototype-version": "1.5.1.1",
212
+ "x-requested-with": "XMLHttpRequest"
213
+ }
214
+ resposta_ajax = await client.post(segunda_url_ajax, data=payload_segundo_ajax, headers=ajax_headers)
215
+ telemetria.bytes_enviados += len(resposta_ajax.text.encode('utf-8'))
216
+ if resposta_ajax.status_code != 200:
217
+ raise Exception(f"Falha ao acessar a URL AJAX: {resposta_ajax.status_code}")
218
+
219
+ # Resolver CAPTCHA
220
+ resultado_ocr = await resolver_captcha(client, url_captcha, telemetria)
221
+
222
+ # Extrair URL de submissão do formulário a partir dos scripts
223
+ script_tags_full = soup_consulta.find_all("script")
224
+ token_url = None
225
+ for script in script_tags_full:
226
+ if script.string:
227
+ m = re.search(r'document\.getElementById\(["\']buscaProcessoForm["\']\)\.action\s*=\s*["\']([^"\']+)["\']', script.string)
228
+ if m:
229
+ relative_url = m.group(1)
230
+ token_url = relative_url if relative_url.startswith("http") else f"{BASE_URL}{relative_url}"
231
+ break
232
+ if not token_url:
233
+ raise Exception("URL de submissão do formulário não encontrada.")
234
+
235
+ # Submissão final do formulário
236
+ final_headers = {
237
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
238
+ "Origin": BASE_URL,
239
+ "Referer": url_final,
240
+ "x-prototype-version": "1.5.1.1",
241
+ "x-requested-with": "XMLHttpRequest"
242
+ }
243
+ payload_form = {
244
+ "processoPageSize": "20",
245
+ "processoPageNumber": "1",
246
+ "processoSortColumn": "p.numeroUnico",
247
+ "processoSortOrder": "asc",
248
+ "codVaraEscolhida": "",
249
+ "opcaoConsultaPublica": "1",
250
+ "flagNumeroUnico": "true",
251
+ "numeroProcesso": numero_processo,
252
+ "codComarca": "-1",
253
+ "tipoCompetencia": "",
254
+ "turma": "",
255
+ "nomeParte": "",
256
+ "cpfCnpj": "",
257
+ "loginAdvogado": "",
258
+ "nomeAdvogado": "",
259
+ "oab": "",
260
+ "oabComplemento": "N",
261
+ "oabUF": "PR",
262
+ "answer": resultado_ocr
263
+ }
264
+ resposta_formulario = await client.post(token_url, data=payload_form, headers=final_headers)
265
+ telemetria.bytes_enviados += len(resposta_formulario.text.encode('utf-8'))
266
+ if resposta_formulario.status_code != 200:
267
+ raise Exception(f"Falha ao submeter o formulário: {resposta_formulario.status_code}")
268
+
269
+ # Chamada AJAX final para obter os resultados
270
+ soup_final = BeautifulSoup(resposta_formulario.text, "html.parser")
271
+ script_tags_final = soup_final.find_all("script", {"type": "text/javascript"})
272
+ token_ajax = extrair_url_token(script_tags_final, r'AjaxJspTag\.HtmlContent\(\s*"([^"]+)"')
273
+ if not token_ajax:
274
+ raise Exception("Token/URL da chamada AjaxJspTag.HtmlContent não encontrado.")
275
+ final_url_ajax = token_ajax if token_ajax.startswith("http") else f"{BASE_URL}{token_ajax}"
276
+ payload_ultimo = {"dummy": "true", "_": ""}
277
+ ultimo_post = await client.post(final_url_ajax, data=payload_ultimo, headers=final_headers)
278
+ telemetria.bytes_enviados += len(ultimo_post.text.encode('utf-8'))
279
+ if ultimo_post.status_code != 200:
280
+ raise Exception(f"Falha ao acessar a URL final: {ultimo_post.status_code}")
281
+
282
+ soup_1 = BeautifulSoup(ultimo_post.text, "html.parser")
283
+ # Verifica se há link para mais movimentações
284
+ if "Clique para visualizar as movimentações mais antigas" in soup_1.text:
285
+ script_tags_extra = soup_1.find_all("script", {"type": "text/javascript"})
286
+ url_desejada = extrair_url_token(script_tags_extra, r'AjaxJspTag\.HtmlContent\(\s*"([^"]+)"')
287
+ if url_desejada:
288
+ full_url = f"{BASE_URL}{url_desejada}"
289
+ resp_final = await client.post(full_url, data=payload_ultimo, headers=final_headers)
290
+ telemetria.bytes_enviados += len(resp_final.text.encode('utf-8'))
291
+ if resp_final.status_code != 200:
292
+ raise Exception(f"Falha ao acessar a URL final: {resp_final.status_code}")
293
+ soup_2 = BeautifulSoup(resp_final.text, "html.parser")
294
+ html_combinado = str(soup_1) + str(soup_2)
295
+ soup_combinado = BeautifulSoup(html_combinado, "html.parser")
296
+ else:
297
+ soup_combinado = soup_1
298
+ else:
299
+ soup_combinado = soup_1
300
+
301
+ movimentacoes = await extrair_movimentacoes(soup_combinado)
302
+ results = {
303
+ "code": 200,
304
+ "message": "SUCESSO",
305
+ "datetime": datetime.now().isoformat(),
306
+ "results": movimentacoes.model_dump(),
307
+ "telemetria": telemetria.model_dump()
308
+ }
309
+
310
+ except Exception as e:
311
+ logger.error(f"Erro durante a consulta: {e}")
312
+ if telemetria.tentativas < TENTATIVAS_MAXIMAS_CAPTCHA:
313
+ logger.info("Tentando resolver o CAPTCHA novamente...")
314
+ telemetria.tentativas += 1
315
+ return await fetch(numero_processo, telemetria)
316
+ else:
317
+ results = {
318
+ 'code': 4,
319
+ 'message': 'ERRO_SERVIDOR_INTERNO',
320
+ 'telemetria': telemetria
321
+ }
322
+ finally:
323
+ telemetria.tempo_total = round(time.time() - inicio_tempo, 2)
324
+ if isinstance(results, dict):
325
+ if "telemetria" not in results:
326
+ results["telemetria"] = telemetria.dict()
327
+ return results
328
+
329
+
330
+ async def research_process(numero_processo: str):
331
+ """
332
+ Interface externa para pesquisa de processo no TJPR.
333
+ """
334
+ str_time = time.time()
335
+ telem = models.Telemetria(tentativas=1, tempo_total=str_time)
336
+ return await fetch(numero_processo, telemetria=telem)
src/monitoramento_processual/monitoramento_tjsc.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from os import environ as env
2
+ from datetime import datetime
3
+ import time
4
+ import os
5
+ import certifi
6
+ from urllib.parse import urljoin
7
+
8
+ from fastapi.logger import logger
9
+ from fastapi.responses import JSONResponse
10
+ import base64
11
+ from fastapi import status
12
+ from bs4 import BeautifulSoup
13
+ import httpx
14
+ from capmonstercloudclient import CapMonsterClient, ClientOptions
15
+ from capmonstercloudclient.requests import TurnstileProxylessRequest
16
+
17
+ # Local imports
18
+ from src.models import ResponseSiteTJSC, Telemetria, MovimentacaoTJSC
19
+ import src.models as models
20
+
21
+
22
+ # Captura variáveis de ambiente e cria constantes
23
+ TEMPO_LIMITE = int(env.get('TEMPO_LIMITE', 180))
24
+ TENTATIVAS_MAXIMAS_CAPTCHA = int(env.get('TENTATIVAS_MAXIMAS_CAPTCHA', 5))
25
+ TENTATIVAS_MAXIMAS_RECURSIVAS = int(env.get('TENTATIVAS_MAXIMAS_RECURSIVAS', 5))
26
+ CAPMONSTER_API_KEY = env.get('CAPMONSTER_API_KEY', "51391018af62079a8f9fc3233de7617b")
27
+
28
+
29
+ # Função para formatar o número do processo
30
+ def formatar_numero_processo(numero_processo):
31
+ return ''.join(filter(str.isdigit, numero_processo))
32
+
33
+
34
+ # Função para capturar todas as movimentações da página HTML
35
+ async def capturar_todas_movimentacoes(pagina_html: str) -> list[MovimentacaoTJSC]:
36
+ soup = BeautifulSoup(pagina_html, "html.parser")
37
+ tabelas = soup.find_all('table', class_='infraTable')
38
+
39
+ tabela_eventos = None
40
+ for tabela in tabelas:
41
+ cabecalhos = [th.get_text(strip=True) for th in tabela.find_all('th')]
42
+ if 'Evento' in cabecalhos and 'Data/Hora' in cabecalhos and 'Descrição' in cabecalhos and 'Documentos' in cabecalhos:
43
+ tabela_eventos = tabela
44
+ break
45
+
46
+ if not tabela_eventos:
47
+ logger.warning("Tabela de eventos não encontrada na página HTML.")
48
+ return []
49
+
50
+ movimentacoes = []
51
+ linhas = tabela_eventos.find_all('tr', class_=['infraTrClara', 'infraTrEscura'])
52
+
53
+ if not linhas:
54
+ logger.warning("Nenhuma linha de eventos encontrada na tabela.")
55
+ return []
56
+
57
+ for linha in linhas:
58
+ celulas = linha.find_all('td')
59
+ if len(celulas) >= 4:
60
+ evento = celulas[0].get_text(strip=True)
61
+ data_hora = celulas[1].get_text(strip=True)
62
+ descricao = celulas[2].get_text(strip=True)
63
+
64
+ link_documento_completo = ""
65
+ link_documento_elemento = celulas[3].find('a', class_='infraLinkDocumento')
66
+ if link_documento_elemento and 'href' in link_documento_elemento.attrs:
67
+ base_url = "https://eprocwebcon.tjsc.jus.br/consulta1g/"
68
+ link_documento = link_documento_elemento['href']
69
+ link_documento_completo = urljoin(base_url, link_documento)
70
+
71
+ movimentacao = MovimentacaoTJSC(
72
+ evento=evento,
73
+ data_hora=data_hora,
74
+ descricao=descricao,
75
+ documentos=link_documento_completo
76
+ )
77
+ movimentacoes.append(movimentacao)
78
+ logger.debug(f"Movimentação capturada: {movimentacao.model_dump()}")
79
+
80
+ return movimentacoes
81
+
82
+
83
+ # Resolve o CAPTCHA Turnstile usando o CapMonster
84
+ async def resolver_captcha_turnstile(website_url: str, site_key: str, page_data: str) -> dict:
85
+ os.environ['SSL_CERT_FILE'] = certifi.where()
86
+
87
+ client_options = ClientOptions(api_key=CAPMONSTER_API_KEY)
88
+ cap_monster_client = CapMonsterClient(options=client_options)
89
+
90
+ turnstile_request = TurnstileProxylessRequest(
91
+ websiteURL=website_url,
92
+ websiteKey=site_key,
93
+ pageData=page_data
94
+ )
95
+
96
+ solution = await cap_monster_client.solve_captcha(turnstile_request)
97
+ return solution
98
+
99
+
100
+ # Função principal para acessar o site do TJSC e capturar as movimentações com httpx
101
+ async def fetch(numero_processo: str, telemetria: Telemetria) -> dict:
102
+ """
103
+ Função que acessa a página inicial do TJSC com httpx, resolve o CAPTCHA Turnstile com retry,
104
+ envia o formulário via POST e captura as movimentações do processo. Retorna os
105
+ resultados como objetos Movimentacao, incluindo links de documentos e a duração
106
+ da requisição.
107
+ """
108
+
109
+ # Validação inicial
110
+ if not numero_processo or not isinstance(numero_processo, str):
111
+ return JSONResponse(
112
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
113
+ content={
114
+ 'code': 2,
115
+ 'message': 'ERRO_ENTIDADE_NAO_PROCESSAVEL'
116
+ }
117
+ )
118
+
119
+ if telemetria.tentativas >= TENTATIVAS_MAXIMAS_RECURSIVAS:
120
+ logger.error("Número máximo de tentativas recursivas atingido.")
121
+ return JSONResponse(
122
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
123
+ content={
124
+ 'code': 3,
125
+ 'message': 'ERRO_SERVIDOR_INTERNO'
126
+ }
127
+ )
128
+
129
+ logger.info(f'Função fetch() iniciou. Processo: {numero_processo} - Tentativa {telemetria.tentativas}')
130
+
131
+ headers = {
132
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
133
+ "Host": "eprocwebcon.tjsc.jus.br",
134
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0"
135
+ }
136
+
137
+ # Criar cliente HTTP para manter cookies e sessão
138
+ client = httpx.Client(timeout=TEMPO_LIMITE, verify=False, headers=headers)
139
+ results = None
140
+
141
+ try:
142
+ # Acessar a página inicial
143
+ url_inicial = "https://eprocwebcon.tjsc.jus.br/consulta1g/externo_controlador.php?acao=processo_consulta_publica"
144
+ response = client.get(url_inicial, follow_redirects=False)
145
+ content_length = response.headers.get('Content-Length')
146
+ if content_length:
147
+ telemetria.bytes_enviados += int(content_length)
148
+ else:
149
+ telemetria.bytes_enviados += len(response.content)
150
+
151
+ if response.status_code != 200:
152
+ raise Exception(f"Falha ao acessar a página inicial: {response.status_code}")
153
+
154
+ page_data = base64.b64encode(response.text.encode('utf-8')).decode('utf-8')
155
+
156
+ soup = BeautifulSoup(response.text, "html.parser")
157
+
158
+ # Verificar se o CAPTCHA Turnstile está presente
159
+ turnstile_div = soup.find("div", class_="cf-turnstile")
160
+ if not turnstile_div:
161
+ raise Exception("Elemento do CAPTCHA Turnstile não encontrado na página inicial.")
162
+
163
+ site_key = turnstile_div.get("data-sitekey")
164
+ if not site_key:
165
+ raise Exception("Chave do site (data-sitekey) não encontrada no elemento Turnstile.")
166
+
167
+ # Resolver o CAPTCHA Turnstile
168
+ telemetria.captchas_resolvidos += 1
169
+ solution = await resolver_captcha_turnstile(url_inicial, site_key, page_data)
170
+
171
+ token = solution.get("token")
172
+
173
+ form_data = {
174
+ "hdnInfraTipPagina": "1",
175
+ "sbmNovo": "Consultar",
176
+ "txtNumProcesso": numero_processo,
177
+ "txtNumChave": "",
178
+ "txtNumChaveDocumentos": "",
179
+ "txtParte": "",
180
+ "chkFonetica": "N",
181
+ "chkFoneticaS": "",
182
+ "txtStrOAB": "",
183
+ "rdTipo": "CPF",
184
+ "cf-turnstile-response": token,
185
+ "hdnInfraCaptcha": "0"
186
+ "hdnInfraSelecoes" "Infra"
187
+ }
188
+ url_post = "https://eprocwebcon.tjsc.jus.br/consulta1g/externo_controlador.php?acao=processo_consulta_publica"
189
+
190
+ response_post = client.post(url_post, data=form_data, follow_redirects=False)
191
+ content_length = response_post.headers.get('Content-Length')
192
+ if content_length:
193
+ telemetria.bytes_enviados += int(content_length)
194
+ else:
195
+ telemetria.bytes_enviados += len(response_post.content)
196
+
197
+ pagina_html = response_post.text
198
+ if "Processo não encontrado" in pagina_html:
199
+ results = {
200
+ 'code': 200,
201
+ 'message': 'Processo não encontrado',
202
+ 'datetime': datetime.now().strftime('%d-%m-%Y %H:%M:%S'),
203
+ 'telemetria': telemetria
204
+ }
205
+ return results
206
+ else:
207
+ # Verificar redirecionamento
208
+ if response_post.status_code == 302:
209
+ redirect_url = response_post.headers["Location"]
210
+ logger.info(f"URL de redirecionamento: {redirect_url}")
211
+ else:
212
+ raise Exception(f"Requisição POST não resultou em redirecionamento: {response_post.status_code}")
213
+
214
+ # Acessar a página de detalhes
215
+ redirect_url = urljoin(url_inicial, redirect_url)
216
+ response_get = client.get(redirect_url, follow_redirects=True)
217
+ content_length = response_get.headers.get('Content-Length')
218
+ if content_length:
219
+ telemetria.bytes_enviados += int(content_length)
220
+ else:
221
+ telemetria.bytes_enviados += len(response_get.content)
222
+
223
+ if response_get.status_code != 200:
224
+ raise Exception(f"Falha ao acessar a página de detalhes: {response_get.status_code}")
225
+
226
+ # Capturar as movimentações
227
+ pagina_html = response_get.text
228
+ if "Processo não encontrado" in pagina_html:
229
+ results = {
230
+ 'code': 200,
231
+ 'message': 'Processo não encontrado',
232
+ 'datetime': datetime.now().strftime('%d-%m-%Y %H:%M:%S'),
233
+ 'telemetria': telemetria
234
+ }
235
+ return results
236
+ else:
237
+ # Verificar redirecionamento
238
+ if response_post.status_code == 302:
239
+ redirect_url = response_post.headers["Location"]
240
+ logger.info(f"URL de redirecionamento: {redirect_url}")
241
+ else:
242
+ raise Exception(f"Requisição POST não resultou em redirecionamento: {response_post.status_code}")
243
+
244
+ # Acessar a página de detalhes
245
+ redirect_url = urljoin(url_inicial, redirect_url)
246
+ response_get = client.get(redirect_url, follow_redirects=True)
247
+ content_length = response_get.headers.get('Content-Length')
248
+ if content_length:
249
+ telemetria.bytes_enviados += int(content_length)
250
+ else:
251
+ telemetria.bytes_enviados += len(response_get.content)
252
+
253
+ if response_get.status_code != 200:
254
+ raise Exception(f"Falha ao acessar a página de detalhes: {response_get.status_code}")
255
+
256
+ # Capturar as movimentações
257
+ pagina_html = response_get.text
258
+ soup_novo = BeautifulSoup(pagina_html, "html.parser")
259
+ link = soup_novo.find(
260
+ 'a',
261
+ href=lambda href: href and "externo_controlador.php?acao=processo_seleciona_publica" in href,
262
+ text=lambda t: t and "listar todos os eventos" in t.lower()
263
+ )
264
+ if link:
265
+
266
+ link_href = link.get("href")
267
+ url_base = "https://eprocwebcon.tjsc.jus.br/consulta1g/"
268
+ url_eventos = urljoin(url_base, link_href)
269
+ response_eventos = client.get(url_eventos, follow_redirects=True, timeout=TEMPO_LIMITE)
270
+ content_length = response_eventos.headers.get('Content-Length')
271
+ if content_length:
272
+ telemetria.bytes_enviados += int(content_length)
273
+ else:
274
+ telemetria.bytes_enviados += len(response_eventos.content)
275
+
276
+ if response_eventos.status_code != 200:
277
+ raise Exception(f"Falha ao acessar a página de eventos: {response_eventos.status_code}")
278
+
279
+ pagina_html = response_eventos.text
280
+
281
+ todas_movimentacoes = await capturar_todas_movimentacoes(pagina_html)
282
+
283
+ else:
284
+ logger.warning("Link para eventos não encontrado na página de detalhes.")
285
+ todas_movimentacoes = await capturar_todas_movimentacoes(pagina_html)
286
+
287
+ # Processar os resultados
288
+ if not todas_movimentacoes or todas_movimentacoes == ["Nenhuma movimentação encontrada na tabela"]:
289
+ logger.error("Nenhuma movimentação encontrada para o processo.")
290
+ results = {
291
+ 'code': 200,
292
+ 'message': 'Nenhuma movimentação encontrada para o processo.',
293
+ 'datetime': datetime.now().strftime('%d-%m-%Y %H:%M:%S'),
294
+ }
295
+ else:
296
+
297
+ logger.info("Processo consultado com sucesso.")
298
+ results = {
299
+ "code": 200,
300
+ "message": "SUCESSO",
301
+ "datetime": datetime.now().isoformat(),
302
+ "results": ResponseSiteTJSC(movimentacoes=todas_movimentacoes).model_dump(),
303
+ "telemetria": telemetria.model_dump()
304
+ }
305
+
306
+ except httpx.RequestError as e:
307
+ logger.error(f"Erro de requisição: {e}")
308
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
309
+ results = JSONResponse(
310
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
311
+ content={
312
+ 'code': 4,
313
+ 'message': 'ERRO_SERVIDOR_INTERNO',
314
+ 'telemetria': telemetria
315
+ }
316
+ )
317
+ except Exception as e:
318
+ logger.error(f"Erro durante a consulta: {e}")
319
+ if telemetria.tentativas < TENTATIVAS_MAXIMAS_CAPTCHA:
320
+ logger.info("Tentando resolver o CAPTCHA novamente...")
321
+ telemetria.tentativas += 1
322
+ return await fetch(numero_processo, telemetria)
323
+ else:
324
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
325
+ results = JSONResponse(
326
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
327
+ content={
328
+ 'code': 4,
329
+ 'message': 'ERRO_SERVIDOR_INTERNO',
330
+ 'telemetria': telemetria
331
+ }
332
+ )
333
+ finally:
334
+ client.close()
335
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
336
+ if results is not None and isinstance(results, dict) and "telemetria" not in results:
337
+ results["telemetria"] = telemetria
338
+
339
+ return results
340
+
341
+ async def research_process(numero_processo: str):
342
+ """
343
+ Interface externa para pesquisa de processo no TJPR.
344
+ """
345
+ str_time = time.time()
346
+ telem = models.Telemetria(tentativas=1, tempo_total=str_time)
347
+ return await fetch(numero_processo, telemetria=telem)
src/monitoramento_processual/monitoramento_tjsp.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from os import environ as env
2
+ from datetime import datetime
3
+ import time
4
+ from urllib.parse import urlparse, urlunparse, urljoin
5
+
6
+ from fastapi.logger import logger
7
+ from fastapi.responses import JSONResponse
8
+ from fastapi import status
9
+ from bs4 import BeautifulSoup
10
+ import httpx
11
+
12
+ # Local imports
13
+ from src.models import ResponseSiteTJSP, Telemetria, MovimentacaoTJSP
14
+ import src.models as models
15
+
16
+ # Captura variáveis de ambiente e cria constantes
17
+ TEMPO_LIMITE = int(env.get('TEMPO_LIMITE', 180))
18
+ TENTATIVAS_MAXIMAS_RECURSIVAS = int(env.get('TENTATIVAS_MAXIMAS_RECURSIVAS', 30))
19
+
20
+
21
+ async def capturar_todas_movimentacoes(pagina_html: str) -> list[MovimentacaoTJSP]:
22
+ """
23
+ Extrai todas as movimentações (visíveis e ocultas) do HTML da página.
24
+ Para cada movimentação, extrai a data, a descrição e o link do documento.
25
+ """
26
+ soup = BeautifulSoup(pagina_html, "html.parser")
27
+ movimentacoes = []
28
+ tbody_ids = ["tabelaUltimasMovimentacoes", "tabelaTodasMovimentacoes"]
29
+
30
+ for tbody_id in tbody_ids:
31
+ tbody = soup.find("tbody", id=tbody_id)
32
+ if not tbody:
33
+ continue
34
+
35
+ for tr in tbody.find_all("tr", class_="containerMovimentacao"):
36
+ td_data = tr.find("td", class_="dataMovimentacao")
37
+ data_text = td_data.get_text(strip=True) if td_data else ""
38
+ td_desc = tr.find("td", class_="descricaoMovimentacao")
39
+ descricao = td_desc.get_text(separator=" ", strip=True) if td_desc else ""
40
+ link_tag = tr.find("a", class_="linkMovVincProc")
41
+ documentos = link_tag.get("href", "") if link_tag else ""
42
+
43
+ movimentacoes.append(
44
+ MovimentacaoTJSP(
45
+ data_hora=data_text,
46
+ descricao=descricao,
47
+ documentos=documentos
48
+ )
49
+ )
50
+
51
+ return ResponseSiteTJSP(movimentacoes=movimentacoes)
52
+
53
+ async def fetch(numero_processo: str, telemetria: Telemetria) -> dict:
54
+ """
55
+ Acessa a página inicial do TJSC, resolve o CAPTCHA com retry, envia o formulário via GET e
56
+ captura as movimentações do processo. Retorna os resultados como objetos Movimentacao,
57
+ incluindo os links completos dos documentos (com ticket).
58
+ """
59
+ if not numero_processo or not isinstance(numero_processo, str):
60
+ return JSONResponse(
61
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
62
+ content={'code': 2, 'message': 'ERRO_ENTIDADE_NAO_PROCESSAVEL'}
63
+ )
64
+
65
+ if telemetria.tentativas >= TENTATIVAS_MAXIMAS_RECURSIVAS:
66
+ logger.error("Número máximo de tentativas recursivas atingido.")
67
+ return JSONResponse(
68
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
69
+ content={'code': 3, 'message': 'ERRO_SERVIDOR_INTERNO'}
70
+ )
71
+
72
+ logger.info(f'Função fetch() iniciou. Processo: {numero_processo} - Tentativa {telemetria.tentativas}')
73
+
74
+ headers = {
75
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
76
+ "Host": "esaj.tjsp.jus.br",
77
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0"
78
+ }
79
+
80
+ client = httpx.Client(timeout=TEMPO_LIMITE, verify=False, headers=headers)
81
+ results = None
82
+ base_url = "https://esaj.tjsp.jus.br"
83
+
84
+ try:
85
+ get_inicial = (
86
+ "https://esaj.tjsp.jus.br/cpopg/search.do?conversationId=&cbPesquisa=NUMPROC"
87
+ "&numeroDigitoAnoUnificado=&foroNumeroUnificado="
88
+ "&dadosConsulta.valorConsultaNuUnificado=&dadosConsulta.valorConsultaNuUnificado=UNIFICADO"
89
+ f"&dadosConsulta.valorConsulta={numero_processo}"
90
+ "&dadosConsulta.tipoNuProcesso=SAJ"
91
+ )
92
+ response = client.get(get_inicial, follow_redirects=False)
93
+ content_length = response.headers.get('Content-Length')
94
+ if content_length:
95
+ telemetria.bytes_enviados += int(content_length)
96
+ else:
97
+ telemetria.bytes_enviados += len(response.content)
98
+
99
+ if response.status_code == 302:
100
+ redirect_url = response.headers.get("Location")
101
+ logger.info(f"Redirecionamento encontrado para: {redirect_url}")
102
+ absolute_url = urljoin(base_url, redirect_url)
103
+ parsed = urlparse(absolute_url)
104
+ clean_path = parsed.path.split(";")[0]
105
+ clean_url = urlunparse((parsed.scheme, parsed.netloc, clean_path, parsed.params, parsed.query, parsed.fragment))
106
+ response = client.get(clean_url, follow_redirects=True)
107
+ content_length = response.headers.get('Content-Length')
108
+ if content_length:
109
+ telemetria.bytes_enviados += int(content_length)
110
+ else:
111
+ telemetria.bytes_enviados += len(response.content)
112
+
113
+ capturar_movimentacoes = await capturar_todas_movimentacoes(response.text)
114
+ results = {
115
+ "code": 200,
116
+ "message": "SUCESSO",
117
+ "datetime": datetime.now().isoformat(),
118
+ "results": capturar_movimentacoes.model_dump(),
119
+ "telemetria": telemetria.model_dump()
120
+ }
121
+
122
+ except httpx.RequestError as e:
123
+ logger.error(f"Erro de requisição: {e}")
124
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
125
+ results = JSONResponse(
126
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
127
+ content={'code': 4, 'message': 'ERRO_SERVIDOR_INTERNO', 'telemetria': telemetria.dict()}
128
+ )
129
+ except Exception as e:
130
+ logger.error(f"Erro durante a consulta: {e}")
131
+ if telemetria.tentativas < TENTATIVAS_MAXIMAS_RECURSIVAS:
132
+ logger.info("Tentando novamente...")
133
+ telemetria.tentativas += 1
134
+ return await fetch(numero_processo, telemetria)
135
+ else:
136
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
137
+ results = JSONResponse(
138
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
139
+ content={'code': 4, 'message': 'ERRO_SERVIDOR_INTERNO', 'telemetria': telemetria.dict()}
140
+ )
141
+ finally:
142
+ client.close()
143
+ telemetria.tempo_total = round(time.time() - telemetria.tempo_total, 2)
144
+ if results is not None and isinstance(results, dict) and "telemetria" not in results:
145
+ results["telemetria"] = telemetria.dict()
146
+
147
+ return results
148
+
149
+ async def research_process(numero_processo: str):
150
+ """
151
+ Interface externa para pesquisa de processo no TJPR.
152
+ """
153
+ str_time = time.time()
154
+ telem = models.Telemetria(tentativas=1, tempo_total=str_time)
155
+ return await fetch(numero_processo, telemetria=telem)
src/monitoramento_processual/task_dispatcher.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.monitoramento_processual import monitoramento_tjpr, monitoramento_tjsc, monitoramento_tjsp
2
+
3
+ async def process_dispatcher(uf: str, current_user: str):
4
+ if uf == "PR":
5
+ resultado = await monitoramento_tjpr.fetch_all(current_user, uf)
6
+ elif uf == "SC":
7
+ resultado = await monitoramento_tjsc.fetch_all(current_user, uf)
8
+ elif uf == "SP":
9
+ resultado = await monitoramento_tjsp.fetch_all(current_user, uf)
10
+ else:
11
+ resultado = {"error": "UF não suportada para monitoramento."}
12
+
13
+ return resultado
src/processamento_tjdf.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import datetime
3
+ import asyncio
4
+ import aiohttp
5
+ from bs4 import BeautifulSoup
6
+
7
+ HEADERS = {
8
+ 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
9
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
10
+ 'Chrome/85.0.4183.83 Safari/537.36')
11
+ }
12
+
13
+ async def fetch(session: aiohttp.ClientSession, url: str):
14
+ """
15
+ Realiza uma requisição GET e retorna o texto da resposta e a URL final.
16
+ """
17
+ async with session.get(url, headers=HEADERS, ssl=False) as response:
18
+ text = await response.text()
19
+ return text, str(response.url)
20
+
21
+ async def parse_detail(session: aiohttp.ClientSession, url_proc: str, numero: str, vara: str) -> dict:
22
+ """
23
+ Processa a página de detalhe do processo e extrai os dados necessários.
24
+ """
25
+ html, _ = await fetch(session, url_proc)
26
+ proc_soup = BeautifulSoup(html, 'html.parser')
27
+
28
+ # Número do processo
29
+ numero_processo_tag = proc_soup.find("span", id="i_numeroProcesso14")
30
+ numero_processo = numero_processo_tag.get_text(strip=True) if numero_processo_tag else numero
31
+
32
+ # Tipo de processo
33
+ feito_codigo_tag = proc_soup.find("span", id="i_feitoCodigo")
34
+ feito_descricao_tag = proc_soup.find("span", id="i_feitoDescricao")
35
+ if feito_codigo_tag and feito_descricao_tag:
36
+ tipo_processo = f"{feito_codigo_tag.get_text(strip=True)} - {feito_descricao_tag.get_text(strip=True)}"
37
+ else:
38
+ tipo_processo = ""
39
+
40
+ # Nome do advogado (autor)
41
+ advogado_tag = proc_soup.find("span", id="i_advogadoAutor")
42
+ nome_advogado = advogado_tag.get_text(strip=True) if advogado_tag else ""
43
+
44
+ # Foro ou endereço da vara
45
+ endereco_vara_tag = proc_soup.find("span", id="i_enderecoVara")
46
+ foro_processo = endereco_vara_tag.get_text(strip=True) if endereco_vara_tag else vara
47
+
48
+ # Extração da data de recebimento a partir dos andamentos
49
+ andamentos = []
50
+ for data_tag in proc_soup.find_all("span", id=re.compile(r"^i_dataHoraAndamento_")):
51
+ num_evento = data_tag.get("id").split("_")[-1]
52
+ desc_tag = proc_soup.find("span", id=f"i_descricaoAndamento_{num_evento}")
53
+ if desc_tag:
54
+ desc = desc_tag.get_text(strip=True)
55
+ date_str = data_tag.get_text(strip=True)
56
+ try:
57
+ dt = datetime.datetime.strptime(date_str, "%d/%m/%Y - %H:%M:%S")
58
+ andamentos.append((dt, desc))
59
+ except ValueError:
60
+ continue
61
+
62
+ recebimento_candidates = [dt for dt, desc in andamentos
63
+ if "007" in desc and "Distribuidos ao cartorio" in desc]
64
+ if recebimento_candidates:
65
+ dt_recebimento = min(recebimento_candidates)
66
+ elif andamentos:
67
+ dt_recebimento = min([dt for dt, _ in andamentos])
68
+ else:
69
+ dt_recebimento = None
70
+ data_recebimento = dt_recebimento.strftime("%d/%m/%Y - %H:%M:%S") if dt_recebimento else ""
71
+
72
+ dados = {
73
+ "nome_advogado": nome_advogado,
74
+ "numero_processo": numero_processo,
75
+ "tipo_processo": tipo_processo,
76
+ "data_recebimento": data_recebimento,
77
+ "link_processo": url_proc,
78
+ "foro_processo": foro_processo,
79
+ "grau_processo": "TJDF - 1ª Instância"
80
+ }
81
+ return dados
82
+
83
+ async def tjdf_request(nome: str) -> list:
84
+ """
85
+ Consulta os processos no TJDF usando requisições assíncronas.
86
+ Retorna uma lista de dicionários com os dados dos processos encontrados.
87
+ """
88
+ dados_processos = []
89
+ base_url = "https://cache-internet.tjdft.jus.br/cgi-bin/tjcgi1"
90
+ search_params = f"?NXTPGM=tjhtml101&ORIGEM=INTER&SELECAO=9&CIRC=ZZ&CHAVE={nome}&CHAVE1=&submit=Consultar"
91
+ url_search = base_url + search_params
92
+
93
+ async with aiohttp.ClientSession() as session:
94
+ html, current_url = await fetch(session, url_search)
95
+ soup = BeautifulSoup(html, "html.parser")
96
+
97
+ # Se nenhum processo foi localizado, retorna lista vazia
98
+ if "0 processo(s) localizado(s)" in soup.text:
99
+ return dados_processos
100
+
101
+ # Se já é uma página de detalhe
102
+ if "Advogado Autor" in soup.text:
103
+ proc_soup = soup
104
+ url_proc = current_url
105
+ vara_tag = proc_soup.find("span", id="i_descricaoVara")
106
+ vara = vara_tag.get_text(strip=True) if vara_tag else ""
107
+
108
+ numero_processo_tag = proc_soup.find("span", id="i_numeroProcesso14")
109
+ if numero_processo_tag:
110
+ numero_processo = numero_processo_tag.get_text(strip=True)
111
+ else:
112
+ hidden_tag = proc_soup.find("hidden", id="detalhamentoDeProcesso")
113
+ if hidden_tag and hidden_tag.has_attr("value"):
114
+ parts = hidden_tag["value"].split(",")
115
+ numero_processo = parts[1].strip() if len(parts) >= 2 else nome
116
+ else:
117
+ numero_processo = nome
118
+
119
+ feito_codigo_tag = proc_soup.find("span", id="i_feitoCodigo")
120
+ feito_descricao_tag = proc_soup.find("span", id="i_feitoDescricao")
121
+ if feito_codigo_tag and feito_descricao_tag:
122
+ tipo_processo = f"{feito_codigo_tag.get_text(strip=True)} - {feito_descricao_tag.get_text(strip=True)}"
123
+ else:
124
+ tipo_processo = ""
125
+
126
+ advogado_tag = proc_soup.find("span", id="i_advogadoAutor")
127
+ nome_advogado = advogado_tag.get_text(strip=True) if advogado_tag else ""
128
+
129
+ endereco_vara_tag = proc_soup.find("span", id="i_enderecoVara")
130
+ foro_processo = endereco_vara_tag.get_text(strip=True) if endereco_vara_tag else vara
131
+
132
+ # Extração dos andamentos
133
+ andamentos = []
134
+ for data_tag in proc_soup.find_all("span", id=re.compile(r"^i_dataHoraAndamento_")):
135
+ num_evento = data_tag.get("id").split("_")[-1]
136
+ desc_tag = proc_soup.find("span", id=f"i_descricaoAndamento_{num_evento}")
137
+ if desc_tag:
138
+ desc = desc_tag.get_text(strip=True)
139
+ date_str = data_tag.get_text(strip=True)
140
+ try:
141
+ dt = datetime.datetime.strptime(date_str, "%d/%m/%Y - %H:%M:%S")
142
+ andamentos.append((dt, desc))
143
+ except ValueError:
144
+ continue
145
+
146
+ recebimento_candidates = [dt for dt, desc in andamentos
147
+ if "007" in desc and "Distribuidos ao cartorio" in desc]
148
+ if recebimento_candidates:
149
+ dt_recebimento = min(recebimento_candidates)
150
+ elif andamentos:
151
+ dt_recebimento = min([dt for dt, _ in andamentos])
152
+ else:
153
+ dt_recebimento = None
154
+ data_recebimento = dt_recebimento.strftime("%d/%m/%Y - %H:%M:%S") if dt_recebimento else ""
155
+
156
+ dados = {
157
+ "nome_advogado": nome_advogado,
158
+ "numero_processo": numero_processo,
159
+ "tipo_processo": tipo_processo,
160
+ "data_recebimento": data_recebimento,
161
+ "link_processo": url_proc,
162
+ "foro_processo": foro_processo,
163
+ "grau_processo": "TJDF - 1ª Instância"
164
+ }
165
+ dados_processos.append(dados)
166
+ return dados_processos
167
+
168
+ # Se for uma página com a lista de processos
169
+ processo_spans = soup.find_all("span", id=re.compile(r"^processo_"))
170
+ processos = {}
171
+ for span in processo_spans:
172
+ numero = span.get_text(strip=True)
173
+ if numero not in processos:
174
+ parent_ul = span.find_parent("ul")
175
+ if parent_ul:
176
+ vara_tag = parent_ul.find("span", id=re.compile(r"^vara_"))
177
+ vara = vara_tag.get_text(strip=True) if vara_tag else ""
178
+ else:
179
+ vara = ""
180
+ processos[numero] = vara
181
+
182
+ # Cria tarefas para buscar os detalhes de cada processo de forma assíncrona
183
+ tasks = []
184
+ for numero, vara in processos.items():
185
+ url_proc = f"{base_url}?NXTPGM=tjhtml105&ORIGEM=INTER&SELECAO=1&CIRCUN=1&CDNUPROC={numero}"
186
+ tasks.append(parse_detail(session, url_proc, numero, vara))
187
+ results = await asyncio.gather(*tasks, return_exceptions=True)
188
+ for res in results:
189
+ if isinstance(res, dict):
190
+ dados_processos.append(res)
191
+
192
+ return dados_processos
src/tj_research/tj_researcher.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.monitoramento_processual import monitoramento_tjpr, monitoramento_tjsc, monitoramento_tjsp
2
+
3
+ async def research_process(uf: str, process_number: str, current_user: str = None):
4
+ """
5
+ Pesquisa um processo específico no tribunal de justiça correspondente à UF.
6
+
7
+ Args:
8
+ uf (str): Unidade Federativa (ex.: "PR", "SC", "SP").
9
+ process_number (str): Número do processo a ser pesquisado.
10
+ current_user (str, optional): Usuário atual, para autenticação ou logging, se necessário.
11
+
12
+ Returns:
13
+ dict: JSON com os dados do processo ou um erro se a UF não for suportada.
14
+ """
15
+ if uf == "PR":
16
+ resultado = await monitoramento_tjpr.research_process(process_number)
17
+ elif uf == "SC":
18
+ resultado = await monitoramento_tjsc.research_process(process_number)
19
+ elif uf == "SP":
20
+ resultado = await monitoramento_tjsp.research_process(process_number)
21
+ else:
22
+ resultado = {"error": "UF não suportada para pesquisa de processos."}
23
+
24
+ return resultado
static/consulta.html ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-br">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Consulta de Processo - Monitoramento Judicial</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ font-family: 'Segoe UI', sans-serif;
11
+ background-color: #f4f6f8;
12
+ color: #333;
13
+ }
14
+
15
+ header {
16
+ background-color: #1e3a8a;
17
+ color: white;
18
+ padding: 15px 30px;
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ }
23
+
24
+ header h1 {
25
+ margin: 0;
26
+ font-size: 1.5rem;
27
+ }
28
+
29
+ nav button {
30
+ background-color: white;
31
+ color: #1e3a8a;
32
+ font-size: 1rem;
33
+ padding: 8px 16px;
34
+ margin-left: 20px;
35
+ border: none;
36
+ border-radius: 4px;
37
+ cursor: pointer;
38
+ }
39
+
40
+ nav button:hover {
41
+ background-color: #f0f0f0;
42
+ }
43
+
44
+ .container {
45
+ max-width: 700px;
46
+ margin: 50px auto;
47
+ padding: 20px;
48
+ background: white;
49
+ border-radius: 8px;
50
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
51
+ }
52
+
53
+ h2 {
54
+ text-align: center;
55
+ }
56
+
57
+ .form-group {
58
+ margin-bottom: 20px;
59
+ }
60
+
61
+ .form-group label {
62
+ display: block;
63
+ margin-bottom: 5px;
64
+ }
65
+
66
+ .form-group input, .form-group select {
67
+ width: 100%;
68
+ padding: 10px;
69
+ font-size: 1rem;
70
+ border: 1px solid #ccc;
71
+ border-radius: 4px;
72
+ }
73
+
74
+ button.submit {
75
+ background-color: #1e3a8a;
76
+ color: white;
77
+ padding: 10px 20px;
78
+ font-size: 1rem;
79
+ border: none;
80
+ border-radius: 4px;
81
+ cursor: pointer;
82
+ }
83
+
84
+ button.submit:hover {
85
+ background-color: #163c74;
86
+ }
87
+
88
+ .result {
89
+ margin-top: 30px;
90
+ }
91
+
92
+ .movimentacao {
93
+ background-color: #eef2f7;
94
+ padding: 10px 15px;
95
+ border-radius: 6px;
96
+ margin-bottom: 10px;
97
+ }
98
+
99
+ .data {
100
+ font-weight: bold;
101
+ color: #1e3a8a;
102
+ }
103
+
104
+ #verMaisBtn {
105
+ background-color: #d3d3d3;
106
+ border: none;
107
+ border-radius: 4px;
108
+ padding: 8px 16px;
109
+ margin: 20px auto 0;
110
+ display: none;
111
+ cursor: pointer;
112
+ font-size: 0.95rem;
113
+ }
114
+
115
+ #carregando {
116
+ text-align: center;
117
+ margin-top: 20px;
118
+ font-weight: bold;
119
+ color: #1e3a8a;
120
+ }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <header>
125
+ <h1>Consulta de Processo</h1>
126
+ <nav>
127
+ <button onclick="window.location.href='/dashboard'">Dashboard</button>
128
+ <button onclick="logout()">Sair</button>
129
+ </nav>
130
+ </header>
131
+
132
+ <div class="container">
133
+ <h2>Consultar Processo no Tribunal</h2>
134
+
135
+ <div class="form-group">
136
+ <label for="uf">UF:</label>
137
+ <select id="uf">
138
+ <option value="PR">PR</option>
139
+ <option value="SC">SC</option>
140
+ <option value="SP">SP</option>
141
+ </select>
142
+ </div>
143
+
144
+ <div class="form-group">
145
+ <label for="process-number">Número do Processo:</label>
146
+ <input type="text" id="process-number" placeholder="0000000-00.0000.0.00.0000">
147
+ </div>
148
+
149
+ <button class="submit" onclick="consultarProcesso()">Consultar</button>
150
+
151
+ <div id="carregando"></div>
152
+
153
+ <div id="resultado" class="result"></div>
154
+ <button id="verMaisBtn" onclick="alternarVerMais()">Ver mais</button>
155
+ </div>
156
+
157
+ <script>
158
+ const token = localStorage.getItem('token');
159
+
160
+ function logout() {
161
+ localStorage.clear();
162
+ window.location.href = '/';
163
+ }
164
+
165
+ let todasMovimentacoes = [];
166
+ let mostrandoTodas = false;
167
+
168
+ function renderizarMovimentacoes() {
169
+ const resultado = document.getElementById("resultado");
170
+ resultado.innerHTML = "";
171
+
172
+ const movimentacoesParaMostrar = mostrandoTodas
173
+ ? todasMovimentacoes
174
+ : todasMovimentacoes.slice(0, 5);
175
+
176
+ movimentacoesParaMostrar.forEach(mov => {
177
+ const div = document.createElement("div");
178
+ div.className = "movimentacao";
179
+
180
+ const data = mov.data || mov.data_hora || "";
181
+ const evento = mov.evento || "";
182
+ const descricao = mov.descricao || evento;
183
+ const documentos = mov.documentos || "";
184
+
185
+ div.innerHTML = `
186
+ <div class="data">${data}</div>
187
+ <div style="white-space: pre-wrap; margin: 5px 0;">${descricao}</div>
188
+ ${documentos ? `<a href="${documentos}" target="_blank" style="color: #1e3a8a; text-decoration: none;">📄 Ver documento</a>` : ""}
189
+ `;
190
+ resultado.appendChild(div);
191
+ });
192
+
193
+ const verMaisBtn = document.getElementById("verMaisBtn");
194
+ if (todasMovimentacoes.length > 5) {
195
+ verMaisBtn.style.display = "block";
196
+ verMaisBtn.textContent = mostrandoTodas ? "Ver menos" : "Ver mais";
197
+ } else {
198
+ verMaisBtn.style.display = "none";
199
+ }
200
+ }
201
+
202
+ function alternarVerMais() {
203
+ mostrandoTodas = !mostrandoTodas;
204
+ renderizarMovimentacoes();
205
+ }
206
+
207
+ async function consultarProcesso() {
208
+ const uf = document.getElementById('uf').value;
209
+ const numero = document.getElementById('process-number').value;
210
+
211
+ document.getElementById("carregando").innerText = "Consultando processo...";
212
+ document.getElementById("resultado").innerHTML = "";
213
+ document.getElementById("verMaisBtn").style.display = "none";
214
+
215
+ try {
216
+ const res = await fetch(`/api/monitoramento-processual/consulta/tjs?uf=${uf}&process_number=${encodeURIComponent(numero)}`, {
217
+ headers: { 'Authorization': `Bearer ${token}` }
218
+ });
219
+
220
+ const data = await res.json();
221
+ todasMovimentacoes = data?.results?.movimentacoes || [];
222
+ mostrandoTodas = false;
223
+ renderizarMovimentacoes();
224
+ } catch (e) {
225
+ document.getElementById("resultado").innerText = "Erro ao consultar processo.";
226
+ } finally {
227
+ document.getElementById("carregando").innerText = "";
228
+ }
229
+ }
230
+ </script>
231
+ </body>
232
+ </html>
static/dashboard.html ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-br">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Painel - Monitoramento Judicial</title>
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ font-family: 'Segoe UI', sans-serif;
12
+ background-color: #f4f6f8;
13
+ color: #333;
14
+ }
15
+
16
+ header {
17
+ background-color: #1e3a8a;
18
+ color: white;
19
+ padding: 15px 30px;
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
24
+ }
25
+
26
+ header h1 {
27
+ margin: 0;
28
+ font-size: 1.5rem;
29
+ }
30
+
31
+ nav {
32
+ position: relative;
33
+ }
34
+
35
+ nav button {
36
+ background-color: white;
37
+ color: #1e3a8a;
38
+ font-size: 1rem;
39
+ padding: 8px 16px;
40
+ margin-left: 10px;
41
+ border: none;
42
+ border-radius: 4px;
43
+ cursor: pointer;
44
+ }
45
+
46
+ nav button:hover {
47
+ background-color: #f0f0f0;
48
+ }
49
+
50
+ .user-menu {
51
+ position: absolute;
52
+ right: 0;
53
+ top: 45px;
54
+ background: white;
55
+ border: 1px solid #ccc;
56
+ border-radius: 4px;
57
+ display: none;
58
+ flex-direction: column;
59
+ z-index: 999;
60
+ }
61
+
62
+ .user-menu button {
63
+ width: 100%;
64
+ text-align: left;
65
+ padding: 10px 16px;
66
+ border: none;
67
+ background: white;
68
+ color: #1e3a8a;
69
+ cursor: pointer;
70
+ }
71
+
72
+ .user-menu button:hover {
73
+ background-color: #f0f0f0;
74
+ }
75
+
76
+ .container {
77
+ max-width: 95%;
78
+ margin: 40px auto;
79
+ padding: 20px;
80
+ background: white;
81
+ border-radius: 8px;
82
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
83
+ }
84
+
85
+ .welcome {
86
+ font-size: 1.2rem;
87
+ margin-bottom: 20px;
88
+ }
89
+
90
+ .card {
91
+ background: #f9f9f9;
92
+ padding: 20px;
93
+ margin-bottom: 20px;
94
+ border-radius: 8px;
95
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
96
+ }
97
+
98
+ .grid {
99
+ display: flex;
100
+ gap: 20px;
101
+ }
102
+
103
+ .half {
104
+ flex: 1;
105
+ }
106
+
107
+ table {
108
+ width: 100%;
109
+ border-collapse: collapse;
110
+ margin-top: 10px;
111
+ }
112
+
113
+ th, td {
114
+ padding: 12px;
115
+ border-bottom: 1px solid #ddd;
116
+ text-align: left;
117
+ }
118
+
119
+ th {
120
+ background-color: #f4f4f4;
121
+ }
122
+
123
+ .delete-icon {
124
+ cursor: pointer;
125
+ color: red;
126
+ }
127
+
128
+ .status-andamento {
129
+ color: green;
130
+ font-weight: bold;
131
+ }
132
+
133
+ .status-pausada {
134
+ color: red;
135
+ font-weight: bold;
136
+ }
137
+ </style>
138
+ </head>
139
+
140
+ <body>
141
+ <header>
142
+ <h1>Monitoramento Judicial</h1>
143
+ <nav>
144
+ <button onclick="window.location.href='/tasks'">Tarefas</button>
145
+ <button onclick="window.location.href='/consulta'">
146
+ <span>Consulta</span> 🔍
147
+ </button>
148
+
149
+
150
+ <button onclick="toggleUserMenu()" id="user-button">Usuário</button>
151
+ <div class="user-menu" id="user-menu">
152
+ <button onclick="abrirConfiguracoes()">Configurações</button>
153
+ <button onclick="verSaldo()">Saldo</button>
154
+ <button onclick="logout()">Sair</button>
155
+ </div>
156
+ </nav>
157
+ </header>
158
+
159
+ <div class="container">
160
+ <div class="welcome">Bem-vindo, <span id="user-welcome">usuário</span>!</div>
161
+
162
+ <div class="card">
163
+ <h2>Tarefas em Andamento</h2>
164
+ <table id="tarefas-table">
165
+ <thead>
166
+ <tr>
167
+ <th>Nome da Tarefa</th>
168
+ <th>Status</th>
169
+ <th>Ações</th>
170
+ </tr>
171
+ </thead>
172
+ <tbody></tbody>
173
+ </table>
174
+ </div>
175
+
176
+ <div class="grid">
177
+ <div class="card half">
178
+ <h2>Resultado da Tarefa - Advogados</h2>
179
+ <table id="advogados-table">
180
+ <thead>
181
+ <tr>
182
+ <th>Nome do Advogado</th>
183
+ <th>UF</th>
184
+ <th>Monitoramento</th>
185
+ <th>Processos</th>
186
+ <th>Ações</th>
187
+ </tr>
188
+ </thead>
189
+ <tbody></tbody>
190
+ </table>
191
+ </div>
192
+
193
+ <div class="card half">
194
+ <h2>Resultado da Tarefa - Processos</h2>
195
+ <table id="processos-table">
196
+ <thead>
197
+ <tr>
198
+ <th>Número do Processo</th>
199
+ <th>Data do Andamento</th>
200
+ <th>Descrição</th>
201
+ <th>Documentos</th>
202
+ <th>Status</th>
203
+ <th>Ações</th>
204
+ </tr>
205
+ </thead>
206
+ <tbody></tbody>
207
+ </table>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <script>
213
+ const token = localStorage.getItem("token");
214
+ const username = localStorage.getItem('username');
215
+ const nome = localStorage.getItem('nome');
216
+ const sobrenome = localStorage.getItem('sobrenome');
217
+
218
+ document.getElementById('user-button').innerText = username || 'Usuário';
219
+ document.getElementById('user-welcome').innerText = `${nome || ''} ${sobrenome || ''}`.trim();
220
+
221
+ function toggleUserMenu() {
222
+ const menu = document.getElementById('user-menu');
223
+ menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex';
224
+ }
225
+
226
+ function logout() {
227
+ localStorage.clear();
228
+ window.location.href = '/';
229
+ }
230
+
231
+ function abrirConfiguracoes() {
232
+ window.location.href = "/user_config";
233
+ }
234
+
235
+ function verSaldo() {
236
+ alert("Saldo atual: R$ 0,00 (em desenvolvimento)");
237
+ }
238
+
239
+ async function preencherNome() {
240
+ const res = await fetch(`/api/monitoramento-processual/usuario-info?username=${username}`, {
241
+ headers: { 'Authorization': `Bearer ${token}` }
242
+ });
243
+ const data = await res.json();
244
+ document.getElementById('user-welcome').innerText = `${data.nome} ${data.sobrenome}`;
245
+ }
246
+
247
+ async function carregarTarefas() {
248
+ const res = await fetch('/api/monitoramento-processual/consulta/listar-tarefas', {
249
+ headers: { 'Authorization': `Bearer ${token}` }
250
+ });
251
+ const tarefas = await res.json();
252
+ const tbody = document.querySelector('#tarefas-table tbody');
253
+ tbody.innerHTML = '';
254
+
255
+ tarefas.forEach(tarefa => {
256
+ const statusClass = tarefa.monitoramento === 'sim' ? 'status-andamento' : 'status-pausada';
257
+ const statusLabel = tarefa.monitoramento === 'sim' ? 'Em andamento' : 'Pausada';
258
+
259
+ const row = document.createElement('tr');
260
+ row.innerHTML = `
261
+ <td>${tarefa.nome}</td>
262
+ <td class="${statusClass}">${statusLabel}</td>
263
+ <td><span class="delete-icon" title="Apagar tarefa">🗑️</span></td>
264
+ `;
265
+ tbody.appendChild(row);
266
+ });
267
+ }
268
+
269
+ window.onload = () => {
270
+ preencherNome();
271
+ carregarTarefas();
272
+ };
273
+
274
+ window.onclick = function (e) {
275
+ const menu = document.getElementById("user-menu");
276
+ if (!document.getElementById("user-button").contains(e.target)) {
277
+ menu.style.display = "none";
278
+ }
279
+ };
280
+ </script>
281
+ </body>
282
+
283
+ </html>
static/index.html ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-br">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Monitoramento Judicial - Login</title>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ </head>
10
+
11
+ <body>
12
+ <div class="container">
13
+ <div class="app-title">Monitoramento Judicial</div>
14
+ <div class="description">Consulta e monitoramento de processos judiciais com tecnologia</div>
15
+
16
+ <!-- LOGIN -->
17
+ <div id="login-box">
18
+ <h1>Login</h1>
19
+ <input type="text" id="username" placeholder="Usuário">
20
+ <input type="password" id="password" placeholder="Senha">
21
+ <button onclick="login()">Entrar</button>
22
+ <button id="show-register" onclick="showRegister()">Criar Conta</button>
23
+ </div>
24
+
25
+ <!-- CADASTRO -->
26
+ <div id="register-box" style="display:none;">
27
+ <h2>Criar Conta</h2>
28
+ <input type="text" id="nome" placeholder="Nome">
29
+ <input type="text" id="sobrenome" placeholder="Sobrenome">
30
+ <input type="email" id="email" placeholder="E-mail">
31
+ <input type="text" id="newUsername" placeholder="Usuário">
32
+ <input type="password" id="newPassword" placeholder="Senha">
33
+ <button onclick="register()">Cadastrar</button>
34
+ <button onclick="hideRegister()">Voltar</button>
35
+ </div>
36
+
37
+ <div class="divider"></div>
38
+
39
+ <button onclick="window.location.href='/docs'">📄 Documentação API</button>
40
+
41
+ <div id="message" class="message"></div>
42
+ <div class="footer">© 2025 Monitoramento Judicial</div>
43
+ </div>
44
+
45
+ <script>
46
+ function showRegister() {
47
+ document.getElementById('register-box').style.display = 'block';
48
+ document.getElementById('login-box').style.display = 'none';
49
+ }
50
+
51
+ function hideRegister() {
52
+ document.getElementById('register-box').style.display = 'none';
53
+ document.getElementById('login-box').style.display = 'block';
54
+ }
55
+
56
+ async function login() {
57
+ const username = document.getElementById('username').value;
58
+ const password = document.getElementById('password').value;
59
+ const response = await fetch('/api/monitoramento-processual/login', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ username: username, senha: password })
63
+ });
64
+ const data = await response.json();
65
+ const msg = document.getElementById('message');
66
+ if (response.ok) {
67
+ localStorage.setItem('token', data.access_token);
68
+ localStorage.setItem('username', username);
69
+
70
+ // 🔄 Novo: Buscar nome e sobrenome
71
+ const userInfo = await fetch(`/api/monitoramento-processual/usuario-info?username=${username}`, {
72
+ headers: { 'Authorization': `Bearer ${data.access_token}` }
73
+ });
74
+ const user = await userInfo.json();
75
+ localStorage.setItem('nome', user.nome);
76
+ localStorage.setItem('sobrenome', user.sobrenome);
77
+
78
+ msg.style.color = 'green';
79
+ msg.innerText = 'Login bem-sucedido! Redirecionando...';
80
+ setTimeout(() => {
81
+ window.location.href = '/dashboard';
82
+ }, 1000);
83
+ } else {
84
+ msg.style.color = 'red';
85
+ msg.innerText = data.detail || 'Erro no login.';
86
+ }
87
+ }
88
+
89
+ async function register() {
90
+ const nome = document.getElementById('nome').value;
91
+ const sobrenome = document.getElementById('sobrenome').value;
92
+ const email = document.getElementById('email').value;
93
+ const username = document.getElementById('newUsername').value;
94
+ const password = document.getElementById('newPassword').value;
95
+ const response = await fetch('/api/monitoramento-processual/cadastrar-usuario', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ username, senha: password, nome, sobrenome, email })
99
+ });
100
+ const data = await response.json();
101
+ const msg = document.getElementById('message');
102
+ if (response.ok) {
103
+ msg.style.color = 'green';
104
+ msg.innerText = 'Usuário cadastrado com sucesso!';
105
+ hideRegister();
106
+ } else {
107
+ msg.style.color = 'red';
108
+ msg.innerText = data.detail || 'Erro no cadastro.';
109
+ }
110
+ }
111
+ </script>
112
+ </body>
113
+
114
+ </html>
static/script.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const token = localStorage.getItem('token');
2
+ const username = localStorage.getItem('username');
3
+
4
+ if (username) {
5
+ document.getElementById('user-name').innerText = username;
6
+ document.getElementById('user-welcome').innerText = username;
7
+ }
8
+
9
+ function logout() {
10
+ localStorage.clear();
11
+ window.location.href = '/';
12
+ }
13
+
14
+ function showForm(action) {
15
+ const container = document.getElementById('form-container');
16
+ let formHTML = '';
17
+
18
+ switch (action) {
19
+ case 'adv-cad':
20
+ formHTML = `<h3>Cadastrar Advogado</h3>
21
+ <input type='text' id='nome-adv' placeholder='Nome Advogado'>
22
+ <input type='text' id='uf-adv' placeholder='UF'>
23
+ <button onclick='cadastrarAdvogado()'>Cadastrar</button>`;
24
+ break;
25
+ case 'adv-del':
26
+ formHTML = `<h3>Deletar Advogado</h3>
27
+ <input type='text' id='nome-adv-del' placeholder='Nome Advogado'>
28
+ <input type='text' id='uf-adv-del' placeholder='UF'>
29
+ <button onclick='deletarAdvogado()'>Deletar</button>`;
30
+ break;
31
+ case 'proc-cad':
32
+ formHTML = `<h3>Cadastrar Processo</h3>
33
+ <input type='text' id='num-proc' placeholder='Número Processo'>
34
+ <input type='text' id='uf-proc' placeholder='UF'>
35
+ <button onclick='cadastrarProcesso()'>Cadastrar</button>`;
36
+ break;
37
+ case 'proc-del':
38
+ formHTML = `<h3>Deletar Processo</h3>
39
+ <input type='text' id='num-proc-del' placeholder='Número Processo'>
40
+ <input type='text' id='uf-proc-del' placeholder='UF'>
41
+ <button onclick='deletarProcesso()'>Deletar</button>`;
42
+ break;
43
+ }
44
+
45
+ container.innerHTML = formHTML;
46
+ }
47
+
48
+ async function cadastrarAdvogado() {
49
+ const nome = document.getElementById('nome-adv').value;
50
+ const uf = document.getElementById('uf-adv').value;
51
+ const res = await fetch(`/api/monitoramento-processual/consulta/inserir-advogado?nome_advogado=${encodeURIComponent(nome)}&uf=${encodeURIComponent(uf)}`, {
52
+ method: 'POST',
53
+ headers: { 'Authorization': `Bearer ${token}` }
54
+ });
55
+ if (res.ok) {
56
+ alert('Advogado cadastrado!');
57
+ await loadAdvogados();
58
+ } else {
59
+ alert('Erro ao cadastrar advogado.');
60
+ }
61
+ }
62
+
63
+ async function deletarAdvogado() {
64
+ const nome = document.getElementById('nome-adv-del').value;
65
+ const uf = document.getElementById('uf-adv-del').value;
66
+ const res = await fetch(`/api/monitoramento-processual/consulta/deletar-advogado?nome_advogado=${encodeURIComponent(nome)}&uf=${encodeURIComponent(uf)}`, {
67
+ method: 'DELETE',
68
+ headers: { 'Authorization': `Bearer ${token}` }
69
+ });
70
+ if (res.ok) {
71
+ alert('Advogado deletado!');
72
+ await loadAdvogados();
73
+ } else {
74
+ alert('Erro ao deletar advogado.');
75
+ }
76
+ }
77
+
78
+ async function cadastrarProcesso() {
79
+ const num = document.getElementById('num-proc').value;
80
+ const uf = document.getElementById('uf-proc').value;
81
+ const res = await fetch(`/api/monitoramento-processual/consulta/inserir-processo?numero_processo=${encodeURIComponent(num)}&uf=${encodeURIComponent(uf)}`, {
82
+ method: 'POST',
83
+ headers: { 'Authorization': `Bearer ${token}` }
84
+ });
85
+ if (res.ok) {
86
+ alert('Processo cadastrado!');
87
+ await loadProcessos();
88
+ } else {
89
+ alert('Erro ao cadastrar processo.');
90
+ }
91
+ }
92
+
93
+ async function deletarProcesso() {
94
+ const num = document.getElementById('num-proc-del').value;
95
+ const uf = document.getElementById('uf-proc-del').value;
96
+ const res = await fetch(`/api/monitoramento-processual/consulta/deletar-processo?numero_processo=${encodeURIComponent(num)}&uf=${encodeURIComponent(uf)}`, {
97
+ method: 'DELETE',
98
+ headers: { 'Authorization': `Bearer ${token}` }
99
+ });
100
+ if (res.ok) {
101
+ alert('Processo deletado!');
102
+ await loadProcessos();
103
+ } else {
104
+ alert('Erro ao deletar processo.');
105
+ }
106
+ }
107
+
108
+ async function loadAdvogados() {
109
+ const res = await fetch(`/api/monitoramento-processual/consulta/listar-advogados`, {
110
+ headers: { 'Authorization': `Bearer ${token}` }
111
+ });
112
+ const data = await res.json();
113
+ const tbody = document.querySelector('#advogados-table tbody');
114
+ tbody.innerHTML = '';
115
+ data.forEach(item => {
116
+ const row = `<tr><td>${item.nome_advogado}</td><td>${item.uf}</td></tr>`;
117
+ tbody.innerHTML += row;
118
+ });
119
+ }
120
+
121
+ async function loadProcessos() {
122
+ const res = await fetch(`/api/monitoramento-processual/consulta/listar-processos`, {
123
+ headers: { 'Authorization': `Bearer ${token}` }
124
+ });
125
+ const data = await res.json();
126
+ const tbody = document.querySelector('#processos-table tbody');
127
+ tbody.innerHTML = '';
128
+ data.forEach(item => {
129
+ const row = `<tr><td>${item.numero_processo}</td><td>${item.uf}</td></tr>`;
130
+ tbody.innerHTML += row;
131
+ });
132
+ }
133
+
134
+ // Carregar ao abrir
135
+ window.onload = () => {
136
+ loadAdvogados();
137
+ loadProcessos();
138
+ };
static/style.css ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ background: linear-gradient(135deg, #f5f7fa, #c3cfe2);
4
+ display: flex;
5
+ justify-content: center;
6
+ align-items: center;
7
+ height: 100vh;
8
+ margin: 0;
9
+ }
10
+
11
+ .container {
12
+ background: white;
13
+ padding: 40px;
14
+ border-radius: 12px;
15
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
16
+ width: 320px;
17
+ text-align: center;
18
+ }
19
+
20
+ .app-title {
21
+ font-size: 1.8rem;
22
+ margin-bottom: 5px;
23
+ color: #1e3a8a;
24
+ }
25
+
26
+ .description {
27
+ font-size: 0.9rem;
28
+ color: #555;
29
+ margin-bottom: 20px;
30
+ }
31
+
32
+ h1, h2 {
33
+ margin-bottom: 15px;
34
+ }
35
+
36
+ input[type="text"], input[type="password"], input[type="email"] {
37
+ width: 100%;
38
+ padding: 10px;
39
+ margin: 10px 0;
40
+ border: 1px solid #ccc;
41
+ border-radius: 4px;
42
+ }
43
+
44
+ button {
45
+ width: 100%;
46
+ padding: 10px;
47
+ background-color: #1e3a8a;
48
+ color: white;
49
+ border: none;
50
+ border-radius: 4px;
51
+ cursor: pointer;
52
+ margin-top: 10px;
53
+ }
54
+
55
+ button:hover {
56
+ background-color: #3749c1;
57
+ }
58
+
59
+ .message {
60
+ margin-top: 10px;
61
+ color: red;
62
+ }
63
+
64
+ .divider {
65
+ margin: 20px 0;
66
+ border-top: 1px solid #ddd;
67
+ }
68
+
69
+ .footer {
70
+ margin-top: 25px;
71
+ font-size: 0.8rem;
72
+ color: #999;
73
+ }
74
+
75
+ .ver-mais-btn {
76
+ margin: 20px auto 0;
77
+ background: none;
78
+ border: none;
79
+ color: #1e3a8a;
80
+ font-weight: bold;
81
+ cursor: pointer;
82
+ font-size: 0.95rem;
83
+ text-align: center;
84
+ display: none;
85
+ }
86
+
87
+ .ver-mais-btn:hover {
88
+ text-decoration: underline;
89
+ }
90
+
91
+ .movimentacao {
92
+ margin-bottom: 15px;
93
+ padding-bottom: 10px;
94
+ border-bottom: 1px solid #ccc;
95
+ }
96
+
97
+ .hidden {
98
+ display: none;
99
+ }
static/tasks.html ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-br">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Monitoramento Judicial - Tarefas</title>
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ font-family: 'Segoe UI', sans-serif;
12
+ background-color: #f4f6f8;
13
+ color: #333;
14
+ }
15
+
16
+ header {
17
+ background-color: #1e3a8a;
18
+ color: white;
19
+ padding: 15px 30px;
20
+ display: flex;
21
+ justify-content: space-between;
22
+ align-items: center;
23
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
24
+ }
25
+
26
+ header h1 {
27
+ margin: 0;
28
+ font-size: 1.5rem;
29
+ }
30
+
31
+ nav button {
32
+ background-color: white;
33
+ color: #1e3a8a;
34
+ font-size: 1rem;
35
+ padding: 8px 16px;
36
+ margin-left: 20px;
37
+ border: none;
38
+ border-radius: 4px;
39
+ cursor: pointer;
40
+ }
41
+
42
+ nav button:hover {
43
+ background-color: #f0f0f0;
44
+ }
45
+
46
+ .container {
47
+ max-width: 800px;
48
+ margin: 40px auto;
49
+ padding: 20px;
50
+ background: white;
51
+ border-radius: 8px;
52
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
53
+ }
54
+
55
+ .welcome {
56
+ font-size: 1.2rem;
57
+ margin-bottom: 20px;
58
+ }
59
+
60
+ form {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 10px;
64
+ }
65
+
66
+ input, select, button {
67
+ padding: 10px;
68
+ font-size: 1rem;
69
+ }
70
+
71
+ button {
72
+ background-color: #1e3a8a;
73
+ color: white;
74
+ border: none;
75
+ border-radius: 4px;
76
+ cursor: pointer;
77
+ }
78
+
79
+ button:hover {
80
+ background-color: #354ec4;
81
+ }
82
+ </style>
83
+ </head>
84
+
85
+ <body>
86
+ <header>
87
+ <h1>Monitoramento Judicial</h1>
88
+ <nav>
89
+ <span id="user-name">Usuário</span>
90
+ <button onclick="window.location.href='/dashboard'">Dashboard</button>
91
+ <button onclick="logout()">Sair</button>
92
+ </nav>
93
+ </header>
94
+
95
+ <div class="container">
96
+ <div class="welcome">Bem-vindo, <span id="user-welcome">Usuário</span>!</div>
97
+
98
+ <h2>Nova Tarefa</h2>
99
+ <form onsubmit="cadastrarTarefa(event)">
100
+ <label for="tipo">Tipo:</label>
101
+ <select id="tipo">
102
+ <option value="advogado">Advogado</option>
103
+ <option value="processo">Processo</option>
104
+ </select>
105
+
106
+ <label for="nome">Nome/Número:</label>
107
+ <input type="text" id="nome" placeholder="Nome do Advogado ou Número do Processo" required>
108
+
109
+ <label for="uf">UF:</label>
110
+ <input type="text" id="uf" placeholder="UF" required>
111
+
112
+ <label for="monitoramento">Deseja monitoramento?</label>
113
+ <select id="monitoramento">
114
+ <option value="sim">Sim</option>
115
+ <option value="nao">Não</option>
116
+ </select>
117
+
118
+ <label for="intervalo">Intervalo de Monitoramento:</label>
119
+ <input type="number" id="intervalo" placeholder="Ex: 15" required>
120
+
121
+ <label for="unidade">Unidade:</label>
122
+ <select id="unidade">
123
+ <option value="minutos">Minutos</option>
124
+ <option value="horas">Horas</option>
125
+ <option value="dias">Dias</option>
126
+ </select>
127
+
128
+ <button type="submit">Cadastrar Tarefa</button>
129
+ </form>
130
+
131
+ <div id="message"></div>
132
+ </div>
133
+
134
+ <script>
135
+ const token = localStorage.getItem('token');
136
+ const username = localStorage.getItem('username');
137
+ document.getElementById('user-name').innerText = username;
138
+ document.getElementById('user-welcome').innerText = username;
139
+
140
+ function logout() {
141
+ localStorage.clear();
142
+ window.location.href = '/';
143
+ }
144
+
145
+ async function cadastrarTarefa(event) {
146
+ event.preventDefault();
147
+ const tipo = document.getElementById('tipo').value;
148
+ const nome = document.getElementById('nome').value;
149
+ const uf = document.getElementById('uf').value;
150
+ const monitoramento = document.getElementById('monitoramento').value;
151
+ const intervalo = document.getElementById('intervalo').value;
152
+ const unidade = document.getElementById('unidade').value;
153
+
154
+ const response = await fetch(`/api/monitoramento-processual/consulta/inserir-tarefa?tipo=${tipo}&nome=${nome}&uf=${uf}&monitoramento=${monitoramento}&intervalo=${intervalo}&unidade=${unidade}`, {
155
+ method: 'POST',
156
+ headers: { 'Authorization': `Bearer ${token}` }
157
+ });
158
+
159
+ const result = await response.json();
160
+ const msg = document.getElementById('message');
161
+ msg.innerText = result.message || result.detail || 'Erro ao cadastrar.';
162
+ msg.style.color = response.ok ? 'green' : 'red';
163
+
164
+ if (response.ok) {
165
+ document.querySelector('form').reset();
166
+ }
167
+ }
168
+ </script>
169
+ </body>
170
+ </html>
static/user_config.html ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Configuração do Usuário</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ background-color: #f8f9fa;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+
15
+ .container {
16
+ max-width: 500px;
17
+ margin: 40px auto;
18
+ padding: 30px;
19
+ background-color: white;
20
+ border-radius: 10px;
21
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
22
+ }
23
+
24
+ h2 {
25
+ margin-bottom: 20px;
26
+ color: #003366;
27
+ }
28
+
29
+ form {
30
+ margin-bottom: 30px;
31
+ }
32
+
33
+ label {
34
+ display: block;
35
+ margin-bottom: 5px;
36
+ font-weight: bold;
37
+ }
38
+
39
+ input[type="text"],
40
+ input[type="password"] {
41
+ width: 100%;
42
+ padding: 10px;
43
+ margin-bottom: 15px;
44
+ border: 1px solid #ccc;
45
+ border-radius: 5px;
46
+ }
47
+
48
+ button {
49
+ background-color: #003366;
50
+ color: white;
51
+ padding: 10px 15px;
52
+ border: none;
53
+ border-radius: 5px;
54
+ cursor: pointer;
55
+ }
56
+
57
+ button:hover {
58
+ background-color: #0052a3;
59
+ }
60
+
61
+ .message {
62
+ margin-top: 10px;
63
+ font-weight: bold;
64
+ }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <div class="container">
69
+ <h2>Atualizar Nome de Usuário</h2>
70
+ <form id="username-form">
71
+ <label for="newUsername">Novo Nome de Usuário:</label>
72
+ <input type="text" id="newUsername" required />
73
+ <button type="submit">Atualizar</button>
74
+ <div id="usernameMessage" class="message"></div>
75
+ </form>
76
+
77
+ <h2>Atualizar Senha</h2>
78
+ <form id="password-form">
79
+ <label for="currentPassword">Senha Atual:</label>
80
+ <input type="password" id="currentPassword" required />
81
+
82
+ <label for="newPassword">Nova Senha:</label>
83
+ <input type="password" id="newPassword" required />
84
+
85
+ <button type="submit">Atualizar</button>
86
+ <div id="passwordMessage" class="message"></div>
87
+ </form>
88
+ </div>
89
+
90
+ <script>
91
+ const token = localStorage.getItem("token"); ;
92
+
93
+ document.getElementById("username-form").addEventListener("submit", async (e) => {
94
+ e.preventDefault();
95
+ const newUsername = document.getElementById("newUsername").value;
96
+
97
+ const res = await fetch("/api/monitoramento-processual/config/usuario/username", {
98
+ method: "PUT",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ Authorization: `Bearer ${token}`,
102
+ },
103
+ body: JSON.stringify({ new_username: newUsername }),
104
+ });
105
+
106
+ const data = await res.json();
107
+ const msgDiv = document.getElementById("usernameMessage");
108
+ msgDiv.textContent = data.message || data.detail;
109
+ msgDiv.style.color = res.ok ? "green" : "red";
110
+ });
111
+
112
+ document.getElementById("password-form").addEventListener("submit", async (e) => {
113
+ e.preventDefault();
114
+ const currentPassword = document.getElementById("currentPassword").value;
115
+ const newPassword = document.getElementById("newPassword").value;
116
+
117
+ const res = await fetch("/api/monitoramento-processual/config/usuario/password", {
118
+ method: "PUT",
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ Authorization: `Bearer ${token}`,
122
+ },
123
+ body: JSON.stringify({
124
+ current_password: currentPassword,
125
+ new_password: newPassword,
126
+ }),
127
+ });
128
+
129
+ const data = await res.json();
130
+ const msgDiv = document.getElementById("passwordMessage");
131
+ msgDiv.textContent = data.message || data.detail;
132
+ msgDiv.style.color = res.ok ? "green" : "red";
133
+ });
134
+ </script>
135
+ </body>
136
+ </html>
user_autentication/user_bd.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+
3
+
4
+ def initialize_user_table():
5
+ conn = sqlite3.connect("monitoramento.db")
6
+ cursor = conn.cursor()
7
+ cursor.execute('''
8
+ CREATE TABLE IF NOT EXISTS usuarios (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ username TEXT UNIQUE NOT NULL,
11
+ senha_hash TEXT NOT NULL,
12
+ nome TEXT,
13
+ sobrenome TEXT,
14
+ email TEXT
15
+ )
16
+ ''')
17
+ conn.commit()
18
+ conn.close()
19
+
20
+ # Chama a função logo ao iniciar
21
+ initialize_user_table()
user_autentication/user_hash.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from passlib.context import CryptContext
3
+ from jose import jwt, JWTError
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ # Configurações
7
+ SECRET_KEY = "chave-super-secreta"
8
+ ALGORITHM = "HS256"
9
+ ACCESS_TOKEN_EXPIRE_MINUTES = 10
10
+
11
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
12
+
13
+ # Logger setup
14
+ logger = logging.getLogger("auth")
15
+ logger.setLevel(logging.INFO)
16
+ handler = logging.StreamHandler()
17
+ formatter = logging.Formatter("[%(asctime)s] %(levelname)s [%(name)s] - %(message)s", "%Y-%m-%d %H:%M:%S")
18
+ handler.setFormatter(formatter)
19
+ logger.addHandler(handler)
20
+
21
+ def verificar_senha(senha_pura, senha_hash):
22
+ return pwd_context.verify(senha_pura, senha_hash)
23
+
24
+ def gerar_hash(senha):
25
+ return pwd_context.hash(senha)
26
+
27
+ def criar_token(data: dict):
28
+ to_encode = data.copy()
29
+ expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
30
+ to_encode.update({"exp": expire})
31
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
32
+ logger.info(f"Token criado para usuário: {data.get('sub')}")
33
+ return encoded_jwt
34
+
35
+ def verificar_token(token: str):
36
+ logger.info(f"Token recebido para verificação: {token}")
37
+ try:
38
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_exp": False})
39
+ logger.info(f"Payload decodificado: {payload}")
40
+ username = payload.get("sub")
41
+ if username is None:
42
+ logger.error("Campo 'sub' ausente no token!")
43
+ raise Exception("Token inválido")
44
+ logger.info(f"Token válido para usuário: {username}")
45
+ return username
46
+ except JWTError as e:
47
+ logger.error(f"Erro JWT ao decodificar: {str(e)}")
48
+ raise Exception("Token inválido ou expirado")
49
+ except Exception as e:
50
+ logger.error(f"Erro geral na verificação do token: {str(e)}")
51
+ raise Exception("Token inválido ou expirado")
user_autentication/verificar_token.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from user_autentication.user_hash import verificar_token
4
+
5
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/monitoramento-processual/login")
6
+
7
+ def get_current_user(token: str = Depends(oauth2_scheme)):
8
+ try:
9
+ username = verificar_token(token)
10
+ return username
11
+ except Exception as e:
12
+ raise HTTPException(
13
+ status_code=status.HTTP_401_UNAUTHORIZED,
14
+ detail="Token inválido ou expirado"
15
+ )
user_configuration/user_config.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from fastapi import HTTPException, status
3
+ from src.models import UserUpdatePassword, UserUpdateUsername
4
+
5
+ from user_autentication.user_hash import gerar_hash, verificar_senha
6
+
7
+
8
+ async def trocar_username(usuario_atual: str, novo_username: str):
9
+ conn = sqlite3.connect("monitoramento.db")
10
+ cursor = conn.cursor()
11
+
12
+ # Verifica se o novo username já existe
13
+ cursor.execute("SELECT username FROM usuarios WHERE username = ?", (novo_username,))
14
+ if cursor.fetchone():
15
+ conn.close()
16
+ raise HTTPException(
17
+ status_code=status.HTTP_400_BAD_REQUEST,
18
+ detail="Nome de usuário já está em uso."
19
+ )
20
+
21
+ # Atualiza o username no banco
22
+ cursor.execute("UPDATE usuarios SET username = ? WHERE username = ?", (novo_username, usuario_atual))
23
+ if cursor.rowcount == 0:
24
+ conn.close()
25
+ raise HTTPException(
26
+ status_code=status.HTTP_404_NOT_FOUND,
27
+ detail="Usuário atual não encontrado."
28
+ )
29
+
30
+ conn.commit()
31
+ conn.close()
32
+ return {"message": "Username atualizado com sucesso."}
33
+
34
+
35
+ async def trocar_senha(usuario_atual: str, senha_atual: str, nova_senha: str):
36
+ conn = sqlite3.connect("monitoramento.db")
37
+ cursor = conn.cursor()
38
+
39
+ # Busca hash da senha atual
40
+ cursor.execute("SELECT senha_hash FROM usuarios WHERE username = ?", (usuario_atual,))
41
+ row = cursor.fetchone()
42
+ if not row:
43
+ conn.close()
44
+ raise HTTPException(
45
+ status_code=status.HTTP_404_NOT_FOUND,
46
+ detail="Usuário não encontrado."
47
+ )
48
+
49
+ senha_hash = row[0]
50
+ if not verificar_senha(senha_atual, senha_hash):
51
+ conn.close()
52
+ raise HTTPException(
53
+ status_code=status.HTTP_401_UNAUTHORIZED,
54
+ detail="Senha atual incorreta."
55
+ )
56
+
57
+ # Atualiza com nova senha
58
+ nova_hash = gerar_hash(nova_senha)
59
+ cursor.execute("UPDATE usuarios SET senha_hash = ? WHERE username = ?", (nova_hash, usuario_atual))
60
+ conn.commit()
61
+ conn.close()
62
+ return {"message": "Senha atualizada com sucesso."}
utils/util.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ HEADERS = [
4
+ {
5
+ 'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"',
6
+ 'Sec-Ch-Ua-Mobile': '?0',
7
+ 'Sec-Ch-Ua-Platform': '"Windows"',
8
+ 'Upgrade-Insecure-Requests': '1' ,
9
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
10
+ },
11
+ {
12
+ 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Opera GX";v="106"',
13
+ 'Sec-Ch-Ua-Mobile': '?0',
14
+ 'Sec-Ch-Ua-Platform': '"Windows"',
15
+ 'Upgrade-Insecure-Requests': '1' ,
16
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0',
17
+ }
18
+ ]
19
+
20
+ #----------------- Headers data -----------------------------------
21
+ def get_headers():
22
+ # 0: Brave broser headers
23
+ # 1: Firefox browser headers
24
+ # 2: Google Chrome browser headers
25
+ return HEADERS[random.randint(0, 1)]