Skill: olegantonov/sigaa (version 1.1.2)

--- name: sigaa description: "Interact with SIGAA (Sistema Integrado de Gestão de Atividades Acadêmicas), the academic management system used by 50+ Brazilian federal universities (UNB, UFRN, UFC, UFPE, UFCG, UFPI, etc.). Use when: (1) checking enrollment status or classes for students, (2) verifying grades or academic history, (3) accessing professor portal (classes, attendance, grade launch), (4) logging in to any SIGAA instance via CAS SSO or direct authentication, (5) automating any SIGAA task via web scraping. Handles both undergrad and graduate (stricto/lato sensu) portals. **REQUIRED ENVIRONMENT VARIABLES:** SIGAA_URL (institution base URL), SIGAA_USER (login/matricula), SIGAA_PASSWORD (password)." metadata: openclaw: requires: bins: - curl - python3 - grep env: - SIGAA_URL - SIGAA_USER - SIGAA_PASSWORD credentials: - name: sigaa-account description: "SIGAA institutional account (matricula/CPF + password)" env: SIGAA_USER,SIGAA_PASSWORD homepage: "https://github.com/olegantonov/sigaa-openclaw-skill" version: "1.1.1" author: "Daniel Marques" license: "MIT" --- # SIGAA Skill SIGAA is a JSF-based web system with no public REST API. All automation uses authenticated web scraping (curl + Python). ## Prerequisites **Required Environment Variables:** ```bash export SIGAA_URL='https://sigaa.unb.br' # Institution base URL (no trailing slash) export SIGAA_USER='241104251' # Login: matricula number or CPF (institution-specific) export SIGAA_PASSWORD='yourpassword' # Your SIGAA password ``` - For username format per institution → see `references/institutions.md` - **Never pass credentials as command-line arguments** — use env vars only ## Quick Start ### 1. Login ```bash source scripts/sigaa_login.sh # Sets $SIGAA_COOKIE_FILE and $SIGAA_USER_ID # Cookie file is chmod 600 and auto-removed on shell exit ``` ### 2. Student Operations ```bash bash scripts/sigaa_student.sh status # Basic info + active program bash scripts/sigaa_student.sh enrollments # Current semester classes bash scripts/sigaa_student.sh enrollment-result # Status of enrollment requests (SUBMETIDA / DEFERIDA / NEGADA) bash scripts/sigaa_student.sh grades # Grades bash scripts/sigaa_student.sh history # Full academic history bash scripts/sigaa_student.sh schedule # Class schedule ``` ### 3. Professor Operations ```bash bash scripts/sigaa_professor.sh classes # Current semester classes bash scripts/sigaa_professor.sh students <turma_id> # Students in a class bash scripts/sigaa_professor.sh attendance # Pending attendance bash scripts/sigaa_professor.sh schedule # Teaching schedule ``` ## JSF Navigation Pattern SIGAA uses JavaServer Faces (JSF) with `jsCookMenu`. All menu actions require a POST with a fresh `ViewState` and `jscook_action`: ```bash VS=$(curl -s -b "$SIGAA_COOKIE_FILE" -c "$SIGAA_COOKIE_FILE" \ "${SIGAA_URL}/sigaa/portais/discente/discente.jsf" | \ grep -oP 'name="javax\.faces\.ViewState"[^>]*value="\K[^"]+' | head -1) curl -s -L -b "$SIGAA_COOKIE_FILE" -c "$SIGAA_COOKIE_FILE" \ -X POST "${SIGAA_URL}/sigaa/portais/discente/discente.jsf" \ -d "menu%3Aform_menu_discente=menu%3Aform_menu_discente" \ -d "id=${SIGAA_USER_ID}" \ --data-urlencode "jscook_action=menu_form_menu_discente_discente_menu:A]#{BEAN.method}" \ --data-urlencode "javax.faces.ViewState=${VS}" ``` Full action reference → `references/student-guide.md` and `references/professor-guide.md` ## Parsing Responses ```python import re, html as h def clean_html(content): content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL) content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL) text = re.sub(r'<[^>]+>', ' ', content) return h.unescape(re.sub(r'[ \t]+', ' ', text)) def extract_table_rows(html_content): content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL) rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL) result = [] for row in rows: cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL) clean = [h.unescape(re.sub(r'<[^>]+>|\s+', ' ', c)).strip() for c in cells if c.strip()] if clean: result.append(clean) return result ``` ## Security Notes - **Credentials via env vars only** — never pass as CLI arguments (prevents shell history leakage) - **Cookie files**: auto-created with `chmod 600`, auto-deleted on shell exit via `trap` - **Rate limiting**: 0.5s delay between requests (built into scripts) - **Session scope**: SIGAA sessions expire after ~20 min inactivity — re-source login if you get redirected to login page - **Network targets**: scripts only contact `$SIGAA_URL` and the CAS SSO host derived from the login redirect — confirm these match your institution before running ## Common Issues | Symptom | Fix | |---------|-----| | "Credenciais inválidas" | Check username format for institution (see `references/institutions.md`) | | "Nenhuma turma neste semestre" | Run `enrollment-result` — status may be SUBMETIDA (pending) | | JSF POST returns portal page | ViewState stale — re-fetch portal before POST | | Session redirect to login | Cookie expired — re-source `sigaa_login.sh` | | "Você não pode tentar re-enviar" | Reused LT token — always fetch a fresh login page | ## References - `references/institutions.md` — Supported institutions, login URLs, username formats - `references/student-guide.md` — Full student portal guide, all JSF actions, parsing tips - `references/professor-guide.md` — Professor portal, grade/attendance workflows

path: README.md
content: # SIGAA OpenClaw Skill

An OpenClaw agent skill for interacting with **SIGAA** (Sistema Integrado de Gestão de Atividades Acadêmicas), the academic management system used by 50+ Brazilian federal universities and institutes.

## Features

- 🔐 **Secure authentication**: CAS SSO (UNB-style) and direct login — credentials via env vars only, never CLI args
- 🎓 **Student Portal**: enrollment status, grades, academic history, class schedule
- 👨🏫 **Professor Portal**: class management, attendance, grade entry
- 🏛️ **Multi-institution**: UNB, UFRN, UFC, UFPE, UFCG, UFPI, UFRRJ, and 40+ more
- 🔒 **Security-first**: cookies chmod 600 + auto-deleted on exit, password cleared from env after login, rate limiting built-in

## Supported Institutions

50+ Brazilian federal universities including:
- UNB (Universidade de Brasília)
- UFRN (Universidade Federal do Rio Grande do Norte — original SIGAA developer)
- UFC, UFPE, UFCG, UFPI, UFRRJ, UFBA, UFPA, UFPB, and many more

## Requirements

- `curl` — HTTP client
- `python3` — HTML parsing
- `grep` with PCRE support (`grep -P`)

## Installation

```bash
clawhub install sigaa
```

## Usage

```bash
# 1. Set credentials as env vars (never pass on command line)
export SIGAA_URL='https://sigaa.unb.br'
export SIGAA_USER='241104251'        # matricula, CPF, or SIAPE depending on institution
export SIGAA_PASSWORD='mypassword'

# 2. Login (source to export session vars into current shell)
source scripts/sigaa_login.sh

# 3. Run operations
bash scripts/sigaa_student.sh enrollment-result   # Check enrollment status
bash scripts/sigaa_student.sh grades              # Grades
bash scripts/sigaa_student.sh status              # Program info
bash scripts/sigaa_professor.sh classes           # Professor: list classes
```

## Security Design

| Concern | Mitigation |
|---------|-----------|
| Password in shell history | Credentials via env vars only — never positional args |
| Cookie exposure | `chmod 600` on creation + `trap EXIT` auto-delete |
| Password in memory | `unset SIGAA_PASSWORD` after successful login |
| Rate limiting / lockout | 0.5s delay between requests |
| Wrong CAS host | Scripts derive CAS host from login redirect — verify before running |

## License

MIT


path: references/institutions.md
content: # SIGAA - Institutions and Login URLs

SIGAA was developed by UFRN (Universidade Federal do Rio Grande do Norte) and is used by 50+ Brazilian federal universities and institutes.

## Login URL Patterns

| Institution | SIGAA Base URL | CAS/Login URL | Notes |
|-------------|---------------|----------------|-------|
| UNB | https://sigaa.unb.br | https://autenticacao.unb.br/sso-server | Uses CAS SSO; username = matricula number (e.g. 241104251) |
| UFRN | https://sigaa.ufrn.br | https://sigaa.ufrn.br/sigaa/verTelaLogin.do | Direct login |
| UFC | https://si3.ufc.br | https://si3.ufc.br/sigaa/verTelaLogin.do | Direct login |
| UFPE | https://sigaa.ufpe.br | https://sigaa.ufpe.br/sigaa/verTelaLogin.do | Direct login |
| UFCG | https://sigaa.ufcg.edu.br | https://sigaa.ufcg.edu.br/sigaa/verTelaLogin.do | Direct login |
| UFPI | https://sigaa.ufpi.br | https://sigaa.ufpi.br/sigaa/verTelaLogin.do | Direct login |
| UFRRJ | https://sigaa.ufrrj.br | https://sigaa.ufrrj.br/sigaa/verTelaLogin.do | Direct login |
| UFSB | https://sigaa.ufsb.edu.br | https://sigaa.ufsb.edu.br/sigaa/verTelaLogin.do | Direct login |
| UFSJ | https://www.sig.ufsj.edu.br | https://www.sig.ufsj.edu.br/sigaa/verTelaLogin.do | Direct login |
| UFPA | https://sigaa.ufpa.br | https://sigaa.ufpa.br/sigaa/verTelaLogin.do | Direct login |
| UFPB | https://sigaa.ufpb.br | https://sigaa.ufpb.br/sigaa/verTelaLogin.do | Direct login |
| UFBA | https://sigaa.ufba.br | https://sigaa.ufba.br/sigaa/verTelaLogin.do | Direct login |
| UFRB | https://sigaa.ufrb.edu.br | https://sigaa.ufrb.edu.br/sigaa/verTelaLogin.do | Direct login |
| IFFarroupilha | https://sig.iffarroupilha.edu.br | — | Instituto Federal |

## Username Formats by Institution

- **UNB**: Matricula number (e.g. `241104251`) — NOT the CPF
- **UFRN**: Usually CPF or login SIAPE (for professors)
- **Most others**: CPF without dots/dashes OR institution registration number

## Authentication Methods

### CAS SSO (UNB and some institutions)
1. GET `https://<cas-server>/sso-server/login?service=https://<sigaa-url>/sigaa/login/cas`
2. Extract: `lt` token, `execution` token, form `action` URL, session cookie
3. POST to form action with: `username`, `password`, `lt`, `execution`, `_eventId=submit`
4. Follow redirect back to SIGAA (receives SIGAA JSESSIONID)

### Direct Login (most institutions)
1. GET `https://<sigaa>/sigaa/verTelaLogin.do`
2. Extract: `javax.faces.ViewState`, session cookie
3. POST to `/sigaa/logar.do` with: `dispatch=logOn`, `user.login`, `user.senha`, `javax.faces.ViewState`

## Portal URLs

| Portal | URL path |
|--------|----------|
| Discente | `/sigaa/portais/discente/discente.jsf` |
| Docente | `/sigaa/verPortalDocente.do` |
| Public | `/sigaa/public/home.jsf` |
| All Classes (turmas) | `/sigaa/portais/discente/turmas.jsf` |


path: references/professor-guide.md
content: # SIGAA Professor Portal - Complete Guide

## Portal Access

Professor portal URL: `/sigaa/verPortalDocente.do`

The docente portal shows:
- Current semester classes (turmas)
- Pending attendance entries
- Research projects (bolsistas, orientandos)
- Extension activities

## JSF Menu Actions (Docente)

All menu POSTs go to `/sigaa/verPortalDocente.do`:

```
POST /sigaa/verPortalDocente.do
Fields:
  menu:form_menu_docente = menu:form_menu_docente
  id = <user_numeric_id>
  jscook_action = <action>
  javax.faces.ViewState = <vs>
```

| Action | jscook_action value |
|--------|-------------------|
| List my classes | `menu_form_menu_docente_docente_menu:A]#{turmaDocente.listar}` |
| Record attendance | `menu_form_menu_docente_docente_menu:A]#{frequenciaAluno.listarTurmasComFrequenciaPendente}` |
| Launch grades | `menu_form_menu_docente_docente_menu:A]#{lancamentoNota.listar}` |
| View schedule | `menu_form_menu_docente_docente_menu:A]#{horarioDocente.visualizarHorario}` |
| Attendance report | `menu_form_menu_docente_docente_menu:A]#{frequenciaAluno.emitirRelatorioFrequencia}` |
| Student list | `menu_form_menu_docente_docente_menu:A]#{turmaDocente.listarAlunos}` |

## Class (Turma) URLs

After listing classes, each turma has an ID that enables direct access:

```
/sigaa/graduacao/turma/discente/lista.jsf?id=<turma_id>
/sigaa/graduacao/frequencia/cadastro/form.jsf?id=<turma_id>
/sigaa/graduacao/nota/lancamento/form.jsf?id=<turma_id>
```

## Grade Entry

Grades are entered per `unidade avaliativa` (evaluation unit), scale 0–10.0.

Grade status in SIGAA:
- `EM ABERTO` — not yet entered
- `CONSOLIDADO` — finalized (cannot change without coordination)
- `HOMOLOGADO` — approved by coordinator

## Attendance (Frequência)

- Minimum attendance: typically 75% (varies by institution/course)
- Record as: present (P), absent (F), absent justified (FJ)
- Submit before deadline — after consolidation, requires coordinator override

## Turma Virtual

Professors can manage content in the virtual classroom:
- Post news/announcements
- Upload materials (PDF, video links)
- Create tasks (tarefas) with deadlines
- Run polls (enquetes)
- Forum management

## Important Deadlines (typical academic calendar)

- Enrollment period: usually weeks 1-2 of semester
- Attendance submission: weekly or at end of semester (varies)
- Grade submission (partial): after each evaluation unit
- Grade consolidation: before official grade deadline (dates vary by institution)


path: references/student-guide.md
content: # SIGAA Student Portal - Complete Guide

## JSF Navigation

SIGAA uses JavaServer Faces (JSF) with the `jsCookMenu` JS library. All menu actions require a POST with:

```
POST /sigaa/portais/discente/discente.jsf
Fields:
  menu:form_menu_discente = menu:form_menu_discente
  id = <user_numeric_id>          # extracted from hidden input on portal page
  jscook_action = <action_string> # menu item action
  javax.faces.ViewState = <vs>    # extracted from hidden input
```

### Key JSF Menu Actions (Discente)

| Action | jscook_action value |
|--------|-------------------|
| Enrollment result | `menu_form_menu_discente_discente_menu:A]#{matriculaGraduacao.verComprovanteSolicitacoes}` |
| Start enrollment (stricto sensu) | `menu_form_menu_discente_discente_menu:A]#{matriculaStrictoBean.iniciar}` |
| Grades report | `menu_form_menu_discente_discente_menu:A]#{relatorioNotasAluno.gerarRelatorio}` |
| Academic history | `menu_form_menu_discente_discente_menu:A]#{portalDiscente.historicoComDadosAdicionais}` |
| Enrollment certificate | `menu_form_menu_discente_discente_menu:A]#{portalDiscente.atestadoMatriculaUnb}` |
| Remove enrollment | `menu_form_menu_discente_discente_menu:A]#{retiradaMatricula.popularSolicitacao}` |
| Lock enrollment | `menu_form_menu_discente_discente_menu:A]#{trancamentoMatricula.popularSolicitacao}` |

### Enrollment Status Values

| Status | Meaning |
|--------|---------|
| SUBMETIDA | Submitted, awaiting processing by coordinator |
| DEFERIDA | Approved by coordinator |
| NEGADA / INDEFERIDA | Rejected |
| CONFIRMADA / APROVADA | Fully confirmed, student is enrolled |
| AGUARDANDO | Waiting (e.g., waiting for prerequisite check) |

### Key Page URLs (direct GET, no menu required)

| Page | URL |
|------|-----|
| Portal discente | `/sigaa/portais/discente/discente.jsf` |
| All virtual classes | `/sigaa/portais/discente/turmas.jsf` |
| Profile | `/sigaa/portais/discente/perfil.jsf` |

## Parsing Tips

### Extract user ID (needed for menu posts)
```bash
grep -oP 'name="id"\s+value="\K[0-9]+' page.html | head -1
```

### Extract ViewState
```bash
grep -oP 'name="javax\.faces\.ViewState"[^>]*value="\K[^"]+' page.html | head -1
```

### Extract table data (Python)
```python
import re, html as h

def extract_tables(html_content):
    # Remove scripts/styles
    content = re.sub(r'<script[^>]*>.*?</script>', '', html_content, flags=re.DOTALL)
    content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL)
    rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
    result = []
    for row in rows:
        cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
        clean = [h.unescape(re.sub(r'<[^>]+>|\s+', ' ', c)).strip() for c in cells]
        clean = [c for c in clean if c]
        if clean:
            result.append(clean)
    return result
```

## Graduate (Stricto Sensu) Specifics

For pós-graduação (mestrado/doutorado), the portal is the same (`/portais/discente/discente.jsf`) but some actions differ:

- Enrollment is via `matriculaStrictoBean` not `matriculaGraduacao`
- Room locations are often listed separately on the program's website (not in SIGAA)
- The "Turmas do Semestre" block in the portal home shows enrolled classes; if empty with SUBMETIDA status, the enrollment is pending coordinator approval

## Common Troubleshooting

| Problem | Likely Cause | Fix |
|---------|-------------|-----|
| "Credenciais inválidas" | Wrong username format | Try matricula number instead of CPF (or vice versa) |
| "Nenhuma turma neste semestre" | Enrollment pending | Check enrollment-result for SUBMETIDA status |
| Session expired / redirect to login | Cookie expired | Re-login with sigaa_login.sh |
| JSF POST returns portal page (no navigation) | Wrong jscook_action or stale ViewState | Re-fetch portal page to get fresh ViewState |


path: scripts/sigaa_login.sh
content: #!/usr/bin/env bash
# sigaa_login.sh - Authenticate to SIGAA via CAS SSO or direct login
#
# USAGE: source scripts/sigaa_login.sh
#
# REQUIRED ENV VARS (set before sourcing):
#   SIGAA_URL      - Institution base URL, e.g. https://sigaa.unb.br
#   SIGAA_USER     - Login (matricula number, CPF, or SIAPE depending on institution)
#   SIGAA_PASSWORD - Account password
#
# EXPORTS (available after sourcing):
#   SIGAA_COOKIE_FILE - Path to session cookie file (chmod 600, auto-deleted on shell exit)
#   SIGAA_USER_ID     - Numeric user ID used in JSF menu POSTs
#   SIGAA_BASE_URL    - Same as SIGAA_URL (convenience alias)
#
# EXAMPLE:
#   export SIGAA_URL='https://sigaa.unb.br'
#   export SIGAA_USER='241104251'
#   export SIGAA_PASSWORD='mypassword'
#   source scripts/sigaa_login.sh
#   bash scripts/sigaa_student.sh enrollment-result

set -euo pipefail

# ---- Validate env vars -------------------------------------------------------

_sigaa_check_env() {
  local missing=()
  [[ -z "${SIGAA_URL:-}" ]]      && missing+=("SIGAA_URL")
  [[ -z "${SIGAA_USER:-}" ]]     && missing+=("SIGAA_USER")
  [[ -z "${SIGAA_PASSWORD:-}" ]] && missing+=("SIGAA_PASSWORD")

  if [[ ${#missing[@]} -gt 0 ]]; then
    echo "ERROR: Missing required environment variables: ${missing[*]}" >&2
    echo "  export SIGAA_URL='https://sigaa.unb.br'" >&2
    echo "  export SIGAA_USER='<your-login>'" >&2
    echo "  export SIGAA_PASSWORD='<your-password>'" >&2
    return 1 2>/dev/null || exit 1
  fi
}

_sigaa_check_env

# ---- Setup cookie file -------------------------------------------------------

SIGAA_COOKIE_FILE="$(mktemp /tmp/sigaa_session_XXXXXX.txt)"
chmod 600 "$SIGAA_COOKIE_FILE"

# Auto-delete cookie file when the shell exits (only when sourced)
trap 'rm -f "$SIGAA_COOKIE_FILE" 2>/dev/null; unset SIGAA_COOKIE_FILE SIGAA_USER_ID SIGAA_BASE_URL' EXIT 2>/dev/null || true

AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"

# ---- Detect login type -------------------------------------------------------

# Follow initial redirect to see if institution uses CAS SSO
INITIAL_URL=$(curl -s -o /dev/null -w "%{url_effective}" -L \
  -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
  -A "$AGENT" \
  "${SIGAA_URL}/sigaa/verTelaLogin.do" 2>/dev/null)

# ---- CAS SSO Login (UNB and others) -----------------------------------------

if echo "$INITIAL_URL" | grep -qiE "autenticacao|sso-server|/cas"; then
  LOGIN_PAGE=$(curl -s \
    -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" "$INITIAL_URL")

  ACTION_PATH=$(echo "$LOGIN_PAGE" | grep -oP 'action="[^"]*"' | head -1 | sed 's/action="//;s/"//')
  LT=$(echo "$LOGIN_PAGE" | grep 'name="lt"' | grep -oP 'value="[^"]*"' | sed 's/value="//;s/"//')
  EXEC=$(echo "$LOGIN_PAGE" | grep 'name="execution"' | grep -oP 'value="[^"]*"' | sed 's/value="//;s/"//')
  CAS_BASE=$(echo "$INITIAL_URL" | grep -oP 'https?://[^/]+')
  FULL_ACTION="${CAS_BASE}${ACTION_PATH}"

  RESULT=$(curl -s -L \
    -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" \
    -X POST "$FULL_ACTION" \
    --data-urlencode "username=${SIGAA_USER}" \
    --data-urlencode "[REDACTED_SECRET]" \
    --data-urlencode "lt=${LT}" \
    --data-urlencode "execution=${EXEC}" \
    -d "_eventId=submit" \
    -w "\n__FINAL_URL__:%{url_effective}")

  if echo "$RESULT" | grep -qiE "credenciais inv|invalid credential|login-error"; then
    echo "ERROR: Invalid credentials for user '${SIGAA_USER}'" >&2
    rm -f "$SIGAA_COOKIE_FILE"
    return 1 2>/dev/null || exit 1
  fi

# ---- Direct Login (most other institutions) ----------------------------------

else
  LOGIN_PAGE=$(curl -s \
    -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" \
    "${SIGAA_URL}/sigaa/verTelaLogin.do")

  VS=$(echo "$LOGIN_PAGE" | grep -oP 'name="javax\.faces\.ViewState"[^>]*value="\K[^"]+' | head -1)

  RESULT=$(curl -s -L \
    -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" \
    -X POST "${SIGAA_URL}/sigaa/logar.do" \
    -d "dispatch=logOn" \
    --data-urlencode "user.login=${SIGAA_USER}" \
    --data-urlencode "user.senha=${SIGAA_PASSWORD}" \
    --data-urlencode "javax.faces.ViewState=${VS}" \
    -w "\n__FINAL_URL__:%{url_effective}")

  if echo "$RESULT" | grep -qiE "senha ou login inv|Usuário e/ou Senha"; then
    echo "ERROR: Invalid credentials for user '${SIGAA_USER}'" >&2
    rm -f "$SIGAA_COOKIE_FILE"
    return 1 2>/dev/null || exit 1
  fi
fi

# ---- Extract user ID ---------------------------------------------------------

PORTAL_HTML=$(curl -s \
  -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
  -A "$AGENT" \
  "${SIGAA_URL}/sigaa/portais/discente/discente.jsf" 2>/dev/null || \
  curl -s \
  -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
  -A "$AGENT" \
  "${SIGAA_URL}/sigaa/verPortalDiscente.do")

SIGAA_USER_ID=$(echo "$PORTAL_HTML" | grep -oP 'name="id"\s+value="\K[0-9]+' | head -1)

export SIGAA_COOKIE_FILE
export SIGAA_USER_ID
export SIGAA_BASE_URL="${SIGAA_URL}"

# ---- Result ------------------------------------------------------------------

if [[ -n "$SIGAA_USER_ID" ]]; then
  echo "OK: Logged in as user ID ${SIGAA_USER_ID} | Cookie: ${SIGAA_COOKIE_FILE} (chmod 600, auto-cleaned)"
else
  echo "WARNING: Logged in but could not extract user ID — some operations may fail"
fi

unset SIGAA_PASSWORD  # Don't keep password in env after login


path: scripts/sigaa_professor.sh
content: #!/usr/bin/env bash
# sigaa_professor.sh - Professor portal operations for SIGAA
#
# REQUIRES (exported by sigaa_login.sh):
#   SIGAA_COOKIE_FILE, SIGAA_USER_ID, SIGAA_BASE_URL
#
# USAGE: bash scripts/sigaa_professor.sh <action>
#
# ACTIONS:
#   classes              - List current semester classes
#   students <turma_id>  - List students in a class
#   attendance           - Show pending attendance entries
#   schedule             - Show professor teaching schedule

set -euo pipefail

AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"

for var in SIGAA_COOKIE_FILE SIGAA_USER_ID SIGAA_BASE_URL; do
  if [[ -z "${!var:-}" ]]; then
    echo "ERROR: \$$var is not set. Run: source scripts/sigaa_login.sh" >&2
    exit 1
  fi
done

_get() {
  sleep 0.5
  curl -s -L -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" -A "$AGENT" "$1"
}

_post_menu() {
  local action="$1"
  sleep 0.5
  local html
  html=$(curl -s -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" "${SIGAA_BASE_URL}/sigaa/verPortalDocente.do")
  local vs
  vs=$(echo "$html" | grep -oP 'name="javax\.faces\.ViewState"[^>]*value="\K[^"]+' | head -1)
  sleep 0.5
  curl -s -L -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" \
    -X POST "${SIGAA_BASE_URL}/sigaa/verPortalDocente.do" \
    -d "menu%3Aform_menu_docente=menu%3Aform_menu_docente" \
    -d "id=${SIGAA_USER_ID}" \
    --data-urlencode "jscook_action=${action}" \
    --data-urlencode "javax.faces.ViewState=${vs}"
}

_extract_table() {
  python3 -c "
import sys, re, html as h
content = sys.stdin.read()
content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL)
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
for row in rows:
    cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
    if cells:
        clean = [h.unescape(re.sub(r'<[^>]+>|\s+', ' ', c)).strip() for c in cells]
        clean = [c for c in clean if c]
        if clean:
            print('\t'.join(clean))
"
}

action_classes() {
  echo "=== Turmas do Docente ==="
  _get "${SIGAA_BASE_URL}/sigaa/verPortalDocente.do" | python3 -c "
import sys, re, html as h
text = sys.stdin.read()
m = re.search(r'(turma.*?)(?:Pesquisa|Extens|$)', text, re.DOTALL | re.IGNORECASE)
if m:
    block = re.sub(r'<[^>]+>', ' ', m.group(1))
    block = h.unescape(re.sub(r'[ \t]+', ' ', block))
    for line in block.split('\n'):
        line = line.strip()
        if line and len(line) > 5:
            print(line)
" | head -40
}

action_students() {
  local turma_id="${1:-}"
  [[ -z "$turma_id" ]] && { echo "Usage: sigaa_professor.sh students <turma_id>" >&2; exit 1; }
  echo "=== Alunos da Turma ${turma_id} ==="
  _get "${SIGAA_BASE_URL}/sigaa/graduacao/turma/discente/lista.jsf?id=${turma_id}" | _extract_table | head -80
}

action_attendance() {
  echo "=== Frequência Pendente ==="
  _post_menu "menu_form_menu_docente_docente_menu:A]#{frequenciaAluno.listarTurmasComFrequenciaPendente}" | _extract_table | head -40
}

action_schedule() {
  echo "=== Grade de Horários ==="
  _post_menu "menu_form_menu_docente_docente_menu:A]#{horarioDocente.visualizarHorario}" | _extract_table | head -40
}

ACTION="${1:-classes}"
shift || true
case "$ACTION" in
  classes)    action_classes ;;
  students)   action_students "$@" ;;
  attendance) action_attendance ;;
  schedule)   action_schedule ;;
  *)
    echo "Unknown action: $ACTION" >&2
    echo "Available: classes | students <turma_id> | attendance | schedule" >&2
    exit 1
    ;;
esac


path: scripts/sigaa_student.sh
content: #!/usr/bin/env bash
# sigaa_student.sh - Student portal operations for SIGAA
#
# REQUIRES (exported by sigaa_login.sh):
#   SIGAA_COOKIE_FILE, SIGAA_USER_ID, SIGAA_BASE_URL
#
# USAGE: bash scripts/sigaa_student.sh <action>
#
# ACTIONS:
#   status            - Show enrollment status and program info
#   enrollments       - List current semester classes
#   enrollment-result - Show enrollment request status (SUBMETIDA / DEFERIDA / NEGADA)
#   grades            - Grades for current/past semesters
#   history           - Full academic history
#   schedule          - Current semester class schedule

set -euo pipefail

AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"

# ---- Guard: check required vars ----------------------------------------------

for var in SIGAA_COOKIE_FILE SIGAA_USER_ID SIGAA_BASE_URL; do
  if [[ -z "${!var:-}" ]]; then
    echo "ERROR: \$$var is not set. Run: source scripts/sigaa_login.sh" >&2
    exit 1
  fi
done

# ---- Helpers -----------------------------------------------------------------

_get() {
  sleep 0.5
  curl -s -L -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" -A "$AGENT" "$1"
}

_post_menu() {
  local action="$1"
  sleep 0.5
  local html
  html=$(curl -s -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" "${SIGAA_BASE_URL}/sigaa/portais/discente/discente.jsf")
  local vs
  vs=$(echo "$html" | grep -oP 'name="javax\.faces\.ViewState"[^>]*value="\K[^"]+' | head -1)

  sleep 0.5
  curl -s -L -c "$SIGAA_COOKIE_FILE" -b "$SIGAA_COOKIE_FILE" \
    -A "$AGENT" \
    -X POST "${SIGAA_BASE_URL}/sigaa/portais/discente/discente.jsf" \
    -d "menu%3Aform_menu_discente=menu%3Aform_menu_discente" \
    -d "id=${SIGAA_USER_ID}" \
    --data-urlencode "jscook_action=${action}" \
    --data-urlencode "javax.faces.ViewState=${vs}"
}

_clean_html() {
  python3 -c "
import sys, re, html as h
t = sys.stdin.read()
t = re.sub(r'<script[^>]*>.*?</script>', '', t, flags=re.DOTALL)
t = re.sub(r'<style[^>]*>.*?</style>', '', t, flags=re.DOTALL)
t = re.sub(r'<[^>]+>', ' ', t)
t = h.unescape(t)
t = re.sub(r'[ \t]+', ' ', t)
t = re.sub(r'\n\s*\n+', '\n', t)
for line in t.split('\n'):
    line = line.strip()
    if line:
        print(line)
"
}

_extract_table() {
  python3 -c "
import sys, re, html as h
content = sys.stdin.read()
content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL)
content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL)
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', content, re.DOTALL)
for row in rows:
    cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
    if cells:
        clean = [h.unescape(re.sub(r'<[^>]+>|\s+', ' ', c)).strip() for c in cells]
        clean = [c for c in clean if c]
        if clean:
            print('\t'.join(clean))
"
}

# ---- Actions -----------------------------------------------------------------

action_status() {
  local html
  html=$(_get "${SIGAA_BASE_URL}/sigaa/portais/discente/discente.jsf")
  echo "$html" | python3 -c "
import sys, re, html as h
text = re.sub(r'<[^>]+>', ' ', sys.stdin.read())
text = h.unescape(re.sub(r'[ \t]+', ' ', text))
keywords = ['PROGRAMA','Curso:','Status:','ATIVO','INATIVO','Mestrado','Doutorado','Graduação','Semestre atual']
seen = set()
for kw in keywords:
    for line in text.split('\n'):
        line = line.strip()
        if kw in line and len(line) < 200 and line not in seen:
            print(line)
            seen.add(line)
            break
"
}

action_enrollments() {
  echo "=== Turmas do Semestre Atual ==="
  local html
  html=$(_get "${SIGAA_BASE_URL}/sigaa/portais/discente/discente.jsf")

  local semester
  semester=$(echo "$html" | grep -oP 'Semestre atual:.*?<strong>\K[^<]+' || echo "")
  [[ -n "$semester" ]] && echo "Semestre: $semester"

  echo "$html" | python3 -c "
import sys, re, html as h
text = sys.stdin.read()
m = re.search(r'turmas-portal(.*?)(?:Comunidades|$)', text, re.DOTALL)
if m:
    block = re.sub(r'<[^>]+>', ' ', m.group(1))
    block = h.unescape(re.sub(r'[ \t]+', ' ', block))
    for line in block.split('\n'):
        line = line.strip()
        if line and line != 'Ver turmas anteriores':
            print(line)
"
}

action_enrollment_result() {
  echo "=== Comprovante de Solicitação de Matrícula ==="
  local html
  html=$(_post_menu "menu_form_menu_discente_discente_menu:A]#{matriculaGraduacao.verComprovanteSolicitacoes}")

  local period
  period=$(echo "$html" | grep -oP 'Per[íi]odo [0-9.]+' | head -1 || echo "")
  [[ -n "$period" ]] && echo "$period"
  echo ""
  echo -e "Componente\tTurma\tSituação"
  echo "---"
  echo "$html" | _extract_table | grep -E "SUBMETIDA|DEFERIDA|NEGADA|APROVADA|INDEFERIDA|AGUARDANDO|[0-9]{6}" || \
    echo "(nenhuma solicitação encontrada)"
}

action_grades() {
  echo "=== Notas ==="
  local html
  html=$(_post_menu "menu_form_menu_discente_discente_menu:A]#{relatorioNotasAluno.gerarRelatorio}")
  echo "$html" | _extract_table | grep -E "[0-9]\.[0-9]|Aprovado|Reprovado|Trancado|Cursando" | head -60 || \
    echo "$html" | _clean_html | grep -E "[0-9]\.[0-9]|Aprovado|Reprovado" | head -30
}

action_history() {
  echo "=== Histórico Acadêmico ==="
  local html
  html=$(_post_menu "menu_form_menu_discente_discente_menu:A]#{portalDiscente.historicoComDadosAdicionais}")
  echo "$html" | _extract_table | grep -E "[0-9]{4}\.[12]|Aprovado|Reprovado|Trancado|Cursando" | head -100
}

action_schedule() {
  echo "=== Turmas Virtuais ==="
  local html
  html=$(_get "${SIGAA_BASE_URL}/sigaa/portais/discente/turmas.jsf")
  echo "$html" | _extract_table | head -50
}

# ---- Dispatch ----------------------------------------------------------------

ACTION="${1:-status}"
case "$ACTION" in
  status)            action_status ;;
  enrollments)       action_enrollments ;;
  enrollment-result) action_enrollment_result ;;
  grades)            action_grades ;;
  history)           action_history ;;
  schedule)          action_schedule ;;
  *)
    echo "Unknown action: $ACTION" >&2
    echo "Available: status | enrollments | enrollment-result | grades | history | schedule" >&2
    exit 1
    ;;
esac


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)